英文:
Swift Architectural Problem with Protocol and Inheritance
问题
I'm more familiar with C++ than I am with Swift, but I'm struggling with getting the architecture right for my problem.
The App Delegate talks to my custom 'MenuSystem' singleton and says 'load the app storyboard and make key & visible' and all that good stuff.
However, the type of menu that the 'MenuSystem' class will use will vary by device. i.e. on iPhone - just as an example - it could be a drawer type menu, but on iPad it might be a tab bar and on macOS it might be a tool bar or side menu…
So the menu system class has a property called 'menuController' and there is a custom protocol that all the actual subclassed View Controllers (i.e. uitableview for drawer, uitabviewcontroller for tab bar etc etc etc)
This protocol defines all the common things that the 'MenuSystem' class needs to communicate to the actual, in use, menu.
The protocol is called 'RootMenuProtocol'
Each of these will be implemented by an actual class e.g. a subclass of UITableViewController that conforms to the 'RootMenuProtocol' protocol.
So in the MenuSystem singleton, I need the property 'self.menuController' to refer to any one of the 3 possible (or more) classes that conform to the protocol.
The problem with this architecture is that when I try and then assign the type of UIViewController currently assigned to the property 'self.menuController' to 'rootWindow?.rootViewController' I get an error: Cannot assign value of type '(any RootMenuProtocol)?' to type 'UIViewController?'.
Here's the code:
App Delegate
import UIKit
@main class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch.
// Create Menu system and launch the first view controller
MenuSystem.shared.loadInitialViewControllerAndMakeActiveInto(Window: window, UsingApplicationStoryboardWithName: "Main")
return true
}
}
RootMenuProtocol
import Foundation
protocol RootMenuProtocol
{
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
}
MenuSystem Singleton
import Foundation
import UIKit
class MenuSystem
{
static let shared: MenuSystem = MenuSystem()
private var menuController: RootMenuProtocol?
private init()
{
}
internal func loadInitialViewControllerAndMakeActiveInto(Window rootWindow: UIWindow?, UsingApplicationStoryboardWithName applicationStoryboardName: String)
{
if UIDevice.current.userInterfaceIdiom == .phone
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
rootWindow?.rootViewController = self.menuController as? UIViewController
rootWindow?.makeKeyAndVisible()
}
else if UIDevice.current.userInterfaceIdiom == .pad
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
rootWindow?.rootViewController = self.menuController as? UIViewController
rootWindow?.makeKeyAndVisible()
}
else if UIDevice.current.userInterfaceIdiom == .mac
{
// Tool Bar here...
}
}
internal func requestTransitionTo(StoryboardID id: String, UsingCustomAnimationTransition transitionAnimation: UIViewControllerAnimatedTransitioning? = nil)
{
self.menuController?.transitionTo(StoryboardID: id, UsingCustomAnimationTransition: transitionAnimation as Any)
}
}
MenuRootTabBarViewController
import UIKit
class MenuRootTabBarViewController: UITabBarController, RootMenuProtocol
{
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
{
}
}
MenuRootTableViewController
import UIKit
class MenuRootTableViewController: UITableViewController, RootMenuProtocol
{
override func viewDidLoad()
{
super.viewDidLoad()
}
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
{
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
}
}
Can someone show me what I've misunderstood about Swift architecture and inheritance/protocols?
I just want the property 'self.menuController' to point to whichever UIViewController subclass I've got that also must conform to the 'RootMenuProtocol'.
英文:
I’m more familiar with C++ than i am with Swift, but I’m struggling with getting the architecture right for my problem.
The App Delegate talks to my custom ‘MenuSystem’ singleton and says ‘load the app storyboard and make key&visible’ and all that good stuff.
However, the type of menu that the ‘MenuSystem’ class will use will vary by device. i.e. on iPhone - just as an example - it could be a drawer type menu, but on iPad it might be a tab bar and on macOS it might be a tool bar or side menu…
So the menu system class has a property called ‘menuController’ and there is a custom protocol that all the actual subclassed View Controllers (i.e. uitableview for drawer, uitabviewcontroller for tab bar etc etc etc)
This protocol defines all the common things that the ‘MenuSystem’ class needs to communicate to the actual, in use, menu.
The protocol is called ‘RootMenuProtocol’
Each of these will be implemented by an actual class e.g. a subclass of UITableViewController that conforms to the ‘RootMenuProtocol’ protocol.
So in the MenuSystem singleton, I need the property ‘self.menuController’ to refer to any one of the 3 possible (or more) classes that conform to the protocol.
The problem with this architecture is that when I try and then assign the type of UIViewController currently assigned to the property ‘self.menuController’ to ‘rootWindow?.rootViewController’ I get an error : Cannot assign value of type '(any RootMenuProtocol)?' to type 'UIViewController?'
Here's the code:
App Delegate
import UIKit
@main class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch.
// Create Menu system and launch first view controller
MenuSystem.shared.loadInitialViewControllerAndMakeActiveInto(Window: window, UsingApplicationStoryboardWithName: "Main")
return true
}
}
RootMenuProtocol
import Foundation
protocol RootMenuProtocol
{
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
}
MenuSystem Singleton
import Foundation
import UIKit
class MenuSystem
{
static let shared: MenuSystem = MenuSystem()
private var menuController: RootMenuProtocol?
private init()
{
}
internal func loadInitialViewControllerAndMakeActiveInto(Window rootWindow: UIWindow?, UsingApplicationStoryboardWithName applicationStoryboardName: String)
{
if UIDevice.current.userInterfaceIdiom == .phone
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
}
else if UIDevice.current.userInterfaceIdiom == .pad
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
}
else if UIDevice.current.userInterfaceIdiom == .mac
{
// Tool Bar here...
}
}
internal func requestTransitionTo(StoryboardID id: String, UsingCustomAnimationTransition transitionAnimation: UIViewControllerAnimatedTransitioning? = nil)
{
self.menuController?.transitionTo(StoryboardID: id, UsingCustomAnimationTransition: transitionAnimation as Any)
}
}
MenuRootTabBarViewController
import UIKit
class MenuRootTabBarViewController: UITabBarController, RootMenuProtocol
{
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
{
}
}
MenuRootTableViewController
import UIKit
class MenuRootTableViewController: UITableViewController, RootMenuProtocol
{
override func viewDidLoad()
{
super.viewDidLoad()
}
func transitionTo(StoryboardID: Any, UsingCustomAnimationTransition: Any)
{
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 0
}
}
Can someone show me what I've misunderstood about Swift architecture and inheritance/protocols?
I just want the property 'self.menuController' to point to whichever UIViewController subclass I've got that also musts conform to the 'RootMenuProtocol'.
答案1
得分: 1
您遇到此错误是因为rootViewController
期望一个UIViewController
,但menuController
可以是任何符合RootMenuProtocol
的东西。没有任何东西表明非视图控制器可以符合RootMenuProtocol
。
如果您确定只会有一个UIViewController
符合RootMenuProtocol
的菜单控制器,那么请将以下代码更改为:
private var menuController: (UIViewController & RootMenuProtocol)?
这表示menuController
必须是一个符合RootMenuProtocol
的UIViewController
(或其子类)。
然后,您可以执行以下操作:
rootWindow?.rootViewController = self.menuController
另一个选项是保留menuController
不变,然后重构以下代码:
if UIDevice.current.userInterfaceIdiom == .phone {
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
} else if UIDevice.current.userInterfaceIdiom == .pad {
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
}
改为:
if UIDevice.current.userInterfaceIdiom == .phone {
let root = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
self.menuController = root
rootWindow?.rootViewController = root
rootWindow?.makeKeyAndVisible()
} else if UIDevice.current.userInterfaceIdiom == .pad {
let root = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
self.menuController = root
rootWindow?.rootViewController = root
rootWindow?.makeKeyAndVisible()
}
这假定MenuRootTabBarViewController
和MenuRootTableViewController
都符合RootMenuProtocol
(否则您也会遇到其他错误)。
英文:
You get the error because rootViewController
expects a UIViewController
but menuController
can be anything that conforms to RootMenuProtocol
. There's nothing that says that a non view controller could conform to RootMenuProtocol
.
If you know you will only ever have a menu controller that is a UIViewController
that conforms to RootMenuProtocol
, then change:
private var menuController: RootMenuProtocol?
to:
private var menuController: (UIViewController & RootMenuProtocol)?
This indicates that menuController
must be a UIViewController
(or a subclass) that also conforms to RootMenuProtocol
.
Now you can do:
rootWindow?.rootViewController = self.menuController
Another option is to leave menuController
as-is and refactor the following:
if UIDevice.current.userInterfaceIdiom == .phone
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
}
else if UIDevice.current.userInterfaceIdiom == .pad
{
self.menuController = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
rootWindow?.rootViewController = self.menuController
rootWindow?.makeKeyAndVisible()
}
to:
if UIDevice.current.userInterfaceIdiom == .phone {
let root = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "drawerRootViewController") as? MenuRootTableViewController
self.menuController = root
rootWindow?.rootViewController = root
rootWindow?.makeKeyAndVisible()
} else if UIDevice.current.userInterfaceIdiom == .pad {
let root = UIStoryboard(name: "PBFMenuSystem", bundle: nil).instantiateViewController(withIdentifier: "tabBarRootViewController") as? MenuRootTabBarViewController
self.menuController = root
rootWindow?.rootViewController = root
rootWindow?.makeKeyAndVisible()
}
This assumes that both MenuRootTabBarViewController
and MenuRootTableViewController
conform to RootMenuProtocol
(which they must or you would have had other errors too).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论