Swift架构问题:协议与继承

huangapple go评论67阅读模式
英文:

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必须是一个符合RootMenuProtocolUIViewController(或其子类)。

然后,您可以执行以下操作:

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()
}

这假定MenuRootTabBarViewControllerMenuRootTableViewController都符合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).

huangapple
  • 本文由 发表于 2023年5月11日 08:18:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/76223349.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定