I'm building a little math game app for two players, for my daughter and friends (but I'm planing to also allow singleplayer later on). For now it's for 2 players and both sit on opposite sides of the iPhone. Each player has the same buttons, same questions to answer. They push a button (of a self build numpad), the vc gets the touch event, it verifies the answer and based on the result a scoreView gets animated and scoreCounters are updated.
The game logic for a side is kind of done. And now I'd like to reuse that logic for one view, and use it twice (same numpad and textfields but on opposite side) But I can't find a away to have such a reusable view with a vc working. I thought I could build the view in a xib connect everything to it's view controller and then reuse that view/vc couple in my storyboard the way I want. But it fails.
I have the following setup at the moment (note: I'm using basic views here and vcs like in this tutorial for now, it's for testing, if all works I will replace the view and vc with the real, bit more complex logic/views):
The relevant files in my "test" setup:
The MessageView.xib has just one label connected as an outlet in the MessageView class, but I'd love to have outlets to labels and buttons in my view controller. The MessageViewController has an outlet to messageView (I'd prefer outlets to views and actions and have the view be the direct view controllers view and have the vc handle all the actions and logic).
In my storyboard I have my RootViewController (will be gameFieldViewController). The RootViewController has a stackView with two containers. Each container VC is of type MessageViewController (will be the calcViewController). The view of that container VC has a subview of type MessageViewWrapper (will be calcView).
My problem now is that in the messageView xib I would like to connect all actions (there's only a label in that example, but it will be my calculator pad with buttons) to my MessageViewController. So that the vc can handle all the logic. But I can only connect the MessageView to the MessageViewController and so I would need to catch all actions in the view and then delegate everything to the MessageViewController I guess, which I'd do if there's no other way, but I still got hope that I just know swift+xcode to little.
I would prefer to have a MessageViewController.xib that does everything like normally build in main.storyboard and then I would like to be able to reuse that controller as often as I want, like in the stackView with multiple containers, each having it's own instance of that vc but it seems to me like as if I could only reuse views, not viewcontroller+view couples.
I tried to set the files owner of the MessageView.xib to be MessageViewController as well as the class of the container view in the Main.storyboard to be MessageViewController to then connect the label directly to the ViewController and modify the text from within the VC but it fails, which I guess is no surprise, but it was the only thing I could think of.
Here's some code:
// 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
Note: Please write a comment if something is off with my question. As you know I have kids, so sometimes I write my text to short or it's nonsense as I often have my children next to me, which often is kind of distracting :D
You can achieve this in a singlestory board via two container views & one view controller. Both container views can have embed segue pointing to the same view controller. I mean, same view controller in the storyboard, but two different instances at runtime.
Open your storyboard:
You should end up with something like this:
Here's the code demonstrating how to work with it:
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!")
}
}
}