带有视图的可重用视图控制器在故事板中(一个场景中有多个)

reusable view controller with view In storyboard (multiple in one scene)

我正在为我的女儿和朋友开发一款供两名玩家使用的小型数学游戏应用程序(但我计划稍后也允许单人游戏)。现在是 2 名玩家,两人都坐在 iPhone 的相对两侧。 每个玩家都有相同的按钮,需要回答相同的问题。他们按下一个按钮(自建小键盘的),vc 获取触摸事件,它验证答案并根据结果生成 scoreView 动画并更新 scoreCounters。

一方的游戏逻辑已经完成。现在我想为一个视图重用该逻辑,并使用它两次(相同的小键盘和文本字段,但在另一侧) 但是我找不到一个可以在 vc 工作的情况下拥有这样一个可重用的视图。我想我可以在 xib 中构建视图,将所有内容连接到它的视图控制器,然后按照我想要的方式在我的故事板中重用 view/vc 对。但是失败了。

我现在有以下设置(注意:我在这里使用基本视图,vcs 就像 this tutorial 现在,它用于测试,如果所有工作我将替换视图和 vc 与真实的,有点复杂 logic/views):

我的“测试”设置中的相关文件:

MessageView.xib 在 MessageView class 中只有一个标签作为出口连接,但我希望在我的视图控制器中有标签和按钮的出口。 MessageViewController 有一个到 messageView 的出口(我更喜欢出口而不是视图和操作,并且让视图成为直接视图控制器视图并让 vc 处理所有操作和逻辑)。

在我的故事板中,我有我的 RootViewController(将是 gameFieldViewController)。 RootViewController 有一个带有两个容器的 stackView。 每个容器 VC 都是 MessageViewController 类型(将是 calcViewController)。 该容器的视图 VC 有一个 MessageViewWrapper 类型的子视图(将是 calcView)。

我现在的问题是,在 messageView xib 中,我想将所有操作(在该示例中只有一个标签,但它将是我的带按钮的计算器板)连接到我的 MessageViewController。这样 vc 就可以处理所有的逻辑。但我只能将 MessageView 连接到 MessageViewController,所以我需要捕获视图中的所有操作,然后将所有内容委托给 MessageViewController 我猜,如果没有,我会这样做其他方式,但我仍然希望我只知道 swift+xcode 到一点点。

我希望有一个 MessageViewController.xib 可以像通常在 main.storyboard 中构建的那样执行所有操作,然后我希望能够像在 stackView 中一样经常重用该控制器有多个容器,每个容器都有自己的 vc 实例,但在我看来,好像我只能重用视图,而不是 viewcontroller+view couples.

问题: 这可能吗?如何 ?如何设置?

我尝试将 MessageView.xib 的文件所有者设置为 MessageViewController,并将 Main.storyboard 中容器视图的 class 设置为 Message ViewController 然后将标签直接连接到 ViewController 并从 VC 中修改文本但它失败了,我想这并不奇怪,但这是我唯一能想到的的。

这是一些代码:

// MessageView.swift
import UIKit

@IBDesignable class MessageViewWrapper : NibWrapperView<MessageView> { }
// wrapper thingy found here 
//https://medium.com/flawless-app-stories/how-to-reuse-complex-xib-designed-views-in-storyboards-using-modern-swift-generics-property-e0b7c06b07a6
class MessageView: UIView {
    
    @IBOutlet weak var messageLabel: UILabel!
    
    var message : String = "" {
        didSet { messageLabel.text = message }
    }
}

// MessageViewController.swift
import UIKit

class MessageViewController: UIViewController {
    @IBOutlet weak var messageView: UIView!
    @IBOutlet weak var messageLabel: UILabel!
    
    override func viewDidLoad() {
        (messageView as? MessageViewWrapper)?.contentView.message = "Yeepee !!!"
    }
    
    func inHere() {
        messageLabel.text = "AHA"
    }
}

// RootViewController
import SwiftUI
class RootViewController: UIViewController {
    var vcTop: MessageViewController!

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if (segue.identifier == "topViewSegue") {
            let vc = segue.destination
            guard let vcTop = vc as? MessageViewController else {return}
            
            vcTop.inHere() // fails
        }
    }
}

// NibWrapperView.swift
import UIKit

/// Class used to wrap a view automatically loaded form a nib file
class NibWrapperView<T: UIView>: UIView {
    /// The view loaded from the nib
    var contentView: T

    required init?(coder: NSCoder) {
        contentView = T.loadFromNib()
        super.init(coder: coder)
        prepareContentView()
    }
    
    override init(frame: CGRect) {
        contentView = T.loadFromNib()
        super.init(frame: frame)
        prepareContentView()
    }
    
    private func prepareContentView() {
        contentView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(contentView)

        contentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        contentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        contentView.prepareForInterfaceBuilder()
    }
}

extension UIView{
    static func loadFromNib() -> Self {
        let bundle = Bundle(for: self)
        let nib = UINib(nibName: String(describing: self), bundle: bundle)
        return nib.instantiate(withOwner: nil, options: nil).first as! Self
    }
}

MessageView.xib

Main.storyboard

注: 如果我的问题有问题,请发表评论。如您所知,我有孩子,所以有时我写的文字很短,或者是胡说八道,因为我的 children 经常放在我旁边,这常常让人分心 :D

您可以通过两个容器视图和一个视图控制器在单层板中实现这一点。两个容器视图都可以嵌入 segue 指向同一个视图控制器。我的意思是,故事板中的视图控制器相同,但在运行时有两个不同的实例。

打开你的故事板:

  • 删除第二个视图控制器
  • 将第二个容器视图(鼠标右键)拖到顶部视图控制器(带有标签的那个)上
  • Select 嵌入
  • 设置新segue的标识符

你应该得到这样的结果:

下面是演示如何使用它的代码:

import UIKit

// You'll get two instances of the same view controller. One for the
// main player, another one for the opponent.
class MessageViewController: UIViewController {
    @IBOutlet private var label: UILabel!
    
    func setMessage(_ message: String) {
        label.text = message
    }
    
    // This is an action from the tap gesture recognizer I did add
    // to the label
    @IBAction func labelClicked(_ sender: UITapGestureRecognizer) {
        print("Label \"\(label.text ?? "N/A")\" clicked")
    }
}

class ViewController: UIViewController {
    @IBOutlet var opponentMessageHostingView: UIView!
    
    private var playerMessageViewController: MessageViewController?
    private var opponentMessageViewController: MessageViewController?
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        switch segue.identifier {
        case "player":
            playerMessageViewController = segue.destination as? MessageViewController
        case "opponent":
            opponentMessageViewController = segue.destination as? MessageViewController
        default:
            break
        }
        
        // At this stage, MessageViewController view is not fully loaded yet. It
        // means that you can't work with the label property here for example.
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // At this stage, MessageViewController view is not fully loaded yet. It
        // means that you can't work with the label property here for example.
        
        // Rotate the opponent view by 180 degrees. We can do this in viewDidLoad,
        // because the opponentMessageHostingView is part of our view controller
        // (not the message one).
        opponentMessageHostingView.transform = CGAffineTransform(rotationAngle: .pi);
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // View is going to appear, message view controllers (embed segue) are
        // fully loaded here
        playerMessageViewController?.setMessage("Player here")
        opponentMessageViewController?.setMessage("Opponent here")
        
        // Don't be bothered with DispatchQueue if you don't what it is yet,
        // it just says that it should execute the code inside {} after
        // 2 seconds.
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.playerMessageViewController?.setMessage("You won!")
            self?.opponentMessageViewController?.setMessage("Game over!")
        }
    }
}