【问题标题】:"Extensions must not contain stored properties" preventing me from refactoring code“扩展不能包含存储的属性”阻止我重构代码
【发布时间】:2020-01-16 13:23:26
【问题描述】:

我有一个 13 行的函数,它在我的应用程序中的每个 ViewController 中重复,整个项目总共有 690 行代码!

/// Adds Menu Button
func addMenuButton() {
    let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
    let menuImage = UIImage(named: "MenuWhite")
    menuButton.setImage(menuImage, for: .normal)

    menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
    self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
@objc func menuTappedAction() {
    coordinator?.openMenu()
}

为了让 menuTappedAction 函数起作用,我必须像这样声明一个弱变量:

extension UIViewController {

weak var coordinator: MainCoordinator?

但是这样做我得到错误Extensions must not contain stored properties 到目前为止我尝试了什么:

1) 删除 weak 关键字将导致我的所有应用程序发生冲突。 2)这样声明:

weak var coordinator: MainCoordinator?
extension UIViewController {

将使错误静音,但协调器不会执行任何操作。有什么建议可以解决这个问题吗?

【问题讨论】:

  • 如果您需要的是单个协调器实例,您可以创建一个单例共享实例。 stackoverflow.com/a/47481780/2303865
  • 这个协调器是如何创建的?是每个人都一样,还是一组控制器独有?

标签: swift refactoring coordinator-pattern


【解决方案1】:

使用NSMapTable 为您的扩展创建一个状态容器,但请确保您指定对键使用弱引用。

创建一个要在其中存储状态的类。我们称之为ExtensionState,然后在扩展文件中创建一个映射作为私有字段。

private var extensionStateMap: NSMapTable<TypeBeingExtended, ExtensionState> = NSMapTable.weakToStrongObjects()

那么你的扩展可以是这样的。

extension TypeBeingExtended {
    private func getExtensionState() -> ExtensionState {
        var state = extensionStateMap.object(forKey: self)

        if state == nil {
            state = ExtensionState()
            extensionStateMap.setObject(state, forKey: self)
        }

        return state
    }

    func toggleFlag() {
        var state = getExtensionState()
        state.flag = !state.flag
    }
}

这适用于 iOS 和 macOS 开发,但不适用于服务器端 Swift,因为那里没有 NSMapTable

【讨论】:

    【解决方案2】:

    这是因为extension 不是一个类,所以它不能包含存储的属性。即使它们是weak 属性。

    考虑到这一点,您有两个主要选择:

    1. 快捷方式:协议+协议扩展
    2. 讨厌的 objc 方式:关联对象

    选项 1:使用协议和协议扩展:

    1.1.声明你的协议

    protocol CoordinatorProtocol: class {
        var coordinator: MainCoordinator? { get set }
        func menuTappedAction()
    }
    

    1.2. 创建一个协议扩展,以便您可以预先实现addMenuButton() 方法

    extension CoordinatorProtocol where Self: UIViewController {
        func menuTappedAction() {
            // Do your stuff here
        }
    }
    

    1.3. 在将采用此协议的类中声明weak var coordinator: MainCoordinator?很遗憾,你不能跳过这个

    class SomeViewController: UIViewController, CoordinatorProtocol {
        weak var coordinator: MainCoordinator?
    }
    

    选项 2:使用 objc 关联对象(不推荐)

    extension UIViewController {
        private struct Keys {
            static var coordinator = "coordinator_key"
        }
    
        public var coordinator: Coordinator? {
            get { objc_getAssociatedObject(self, &Keys.coordinator) as? Coordinator }
            set { objc_setAssociatedObject(self, &Keys.coordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) }
        }
    }
    

    【讨论】:

    • 请注意,如果协调器解除分配,OBJC_ASSOCIATION_ASSIGN 可能会导致崩溃。
    【解决方案3】:

    您可以使用 objc 关联对象。

    extension UIViewController {
        private struct Keys {
            static var coordinator = "coordinator_key"
        }
    
        private class Weak<V: AnyObject> {
            weak var value: V?
    
            init?(_ value: V?) {
                guard value != nil else { return nil }
                self.value = value
            }
        }
    
        var coordinator: Coordinator? {
            get { (objc_getAssociatedObject(self, &Keys.coordinator) as? Weak<Coordinator>)?.value }
            set { objc_setAssociatedObject(self, &Keys.coordinator, Weak(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
        }
    }
    

    【讨论】:

    • 请注意,OBJC_ASSOCIATION_ASSIGN 不存储 weak 引用,它存储 unowned 一个,即使文档另有说明。你可以使用一个盒子来避免这个问题。
    • @Cristik 很有趣,我不知道这个,发布后获取价值会崩溃吗?
    • 是的,它会崩溃:)
    【解决方案4】:

    您可以将您的 addMenuButton() 函数移动到具有协议扩展的协议。例如:

    @objc protocol Coordinated: class {
        var coordinator: MainCoordinator? { get set }
        @objc func menuTappedAction()
    }
    
    extension Coordinated where Self: UIViewController {
        func addMenuButton() {
            let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
            let menuImage = UIImage(named: "MenuWhite")
            menuButton.setImage(menuImage, for: .normal)
    
            menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
            self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
        }
    }
    

    不幸的是,您不能将 @objc 方法添加到类扩展中(请参阅:this stackoverflow question),因此您仍然必须像这样设置视图控制器:

    class SomeViewController: UIViewController, Coordinated {
        weak var coordinator: MainCoordinator?
        /// Launches the MenuViewController
        @objc func menuTappedAction() {
            coordinator?.openMenu()
        }
    }
    

    它将为您节省一些代码,并允许您重构更大的函数addMenuButton()。希望这会有所帮助!

    【讨论】:

      【解决方案5】:

      为了让它在扩展中工作,你必须使它像这样计算属性:-

      extension ViewController {
      
         // Make it computed property
          weak var coordinator: MainCoordinator? {
              return MainCoordinator()
          }
      
      }
      
      

      【讨论】:

      • 这将允许它编译,但它不允许ViewController 的子类实际设置coordinator
      • @vacawama 是的,这就是为什么它是一个计算属性,当 Viewcontroller 初始化时,它会从其实例中自动设置为仅获取属性。
      • 如果这就是所有需要的 OP,他们可以将 coordinator?.openMenu() 替换为 MainCoordinator().openMenu() 并跳过整个变量。
      • 我认为调用 MainCoordinator().openMenu() 将使 MainCoordinator 成为单例,而不仅仅是特定 ViewController 的单独实例,并导致意外结果。因此,在 VC 上创建一个单独的属性使其成为使用它的 VC 专用的单独实例。
      【解决方案6】:

      你可以通过子类化来做到这一点

      class CustomVC:UIViewController {
      
          weak var coordinator: MainCoordinator?
      
          func addMenuButton() {
              let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
              let menuImage = UIImage(named: "MenuWhite")
              menuButton.setImage(menuImage, for: .normal)
      
              menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
              self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
          }
          /// Launches the MenuViewController
          @objc func menuTappedAction() {
              coordinator?.openMenu()
          }
      
      }
      
      class MainCoordinator {
      
          func openMenu() {
      
          }
      }
      
      
      class ViewController: CustomVC {
      
          override func viewDidLoad() {
              super.viewDidLoad()
              // Do any additional setup after loading the view.
          }
      
      }
      

      【讨论】:

      • 感谢您的回答,在我的情况下,继承不起作用,因为我已经从在每个 ViewController 中实例化我的故事板的协议继承。所以这是不允许的多重继承。
      • 同样调用 CustomVC().addMenuButton() 将不会执行任何操作。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-12
      • 2017-11-24
      • 1970-01-01
      相关资源
      最近更新 更多