3

I've written a protocol and corresponding extension which utilizes a simple StringStack in tandem with a naming convention of the form "<origin>@<destination>" to perform segues between nested views in my Swift application. I'm new to swift so if there's a better way to implement what are essentially forward and back buttons across multiple views, I'm open to it. However the following code is problematic for reasons I detail below.

struct StringStack {
    var stack: Array = [String]()

    mutating func push(str: String)->Void {
        self.stack.append(str)
    }

    mutating func pop()->String {
        self.stack.popLast()
    }
}

protocol SegueForwardAndBackward {
    var thisIdentifier: String {get set}
    var segueStack: StringStack {get set}
}

extension SegueForwardAndBackward where Self: UIViewController {

    mutating func performForwardSegue(nextIdentifier: String) {
        let identifier: String = thisIdentifier + "@" + nextIdentifier
        segueStack.push(identifier)
        performSegueWithIdentifier(identifier, sender: self)
    }

    mutating func performBackwardSegue() {
        let divided = segueStack.pop().componentsSeparatedByString("@")
        // reverses destination and origin of identifier
        let segueIdentifier: String = divided[1] + "@" + divided[0]
        performSegueWithIdentifier(segueIdentifier, sender: self)
    }

    func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if var destinationViewController: SegueForwardAndBackward = segue.destinationViewController as? SegueForwardAndBackward {
            destinationViewController.segueStack = segueStack
        }
    }
}

My initial problem is that mutating functions aren't allowed in protocol extension as detailed in this bug report. There is a proposed workaround given in that report however the following results in an error claiming that 'SegueForwardAndBackward' is not a subtype of 'UIViewController'.

class A: UIViewController, SegueForwardAndBackward {

    mutating func testFunc() {
        var p: SegueForwardAndBackward = self
        p.performBackwardSegue()
    }
}

I did come across a potential solution where I could make SegueForwardAndBackward a class protocol as detailed in another question. However, implementing a class protocol results in a segfault, which I presume is because I declare my extension to be where Self: UIViewController, and unfortunately I've no clue how to resolve this.

At the moment I'm quite frustrated with what appears to be yet another bug in Swift, so any help in reworking my approach, or deciphering these errors would be much appreciated.

Thanks in advance for your response!

Community
  • 1
  • 1
rmorshea
  • 832
  • 1
  • 7
  • 25
  • I have more questions here. You have constrained the protocol extension to UIViewController, a reference type, but the compiler still allows `mutating` funcs? How? – Aswath Jul 12 '22 at 09:50

2 Answers2

0

This question seems to address an alternate solution for nested forward and back buttons, so without any other responses, this it what I'll be going with. However, the issue I posed still stands out as a pretty strange bug in swift.

Community
  • 1
  • 1
rmorshea
  • 832
  • 1
  • 7
  • 25
0

On diving a bit deeper, I found that the mutating modifier on the functions on the protocol extensions are not needed in your case, if you add nonmutating on the segueStack. Because you're telling

struct StringStack {
    var stack: Array = [String]()

    mutating func push(str: String)->Void {
        self.stack.append(str)
    }

    mutating func pop()->String? {
        self.stack.popLast()
    }
}

protocol SegueForwardAndBackward {
    var thisIdentifier: String {get set}
    var segueStack: StringStack {get nonmutating set}
}

extension SegueForwardAndBackward where Self: UIViewController {

    func performForwardSegue(nextIdentifier: String) {
        let identifier: String = thisIdentifier + "@" + nextIdentifier
        segueStack.push(str: identifier)
        performSegue(withIdentifier: identifier, sender: self)
    }

    func performBackwardSegue() {
        guard let divided = segueStack.pop()?.components(separatedBy: "@") else { return }
        // reverses destination and origin of identifier
        let segueIdentifier: String = divided[1] + "@" + divided[0]
        performSegue(withIdentifier: segueIdentifier, sender: self)
    }

    func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if var destinationViewController: SegueForwardAndBackward = segue.destination as? SegueForwardAndBackward {
            destinationViewController.segueStack = segueStack
        }
    }
}

And your class would look like

class A: UIViewController, SegueForwardAndBackward {
    
    var thisIdentifier: String
    var segueStack: StringStack
    
    func testFunc() {
        self.performBackwardSegue
    }
    
    init(thisIdentifier: String, segueStack: StringStack) {
        super.init(nibName: "A", bundle: .main)

        self.thisIdentifier = thisIdentifier
        self.segueStack = segueStack
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

However this means that if you conform a struct to SegueForwardAndBackward, you'll need to implement a nonmutating set.

struct B: SegueForwardAndBackward {
    
    var thisIdentifier: String
    var segueStack: StringStack { //Toy example
        get {
            UserDefaults.standard.object(forKey: "someKey") as! StringStack
        }
        nonmutating set {
            UserDefaults.standard.set(newValue, forKey: "someKey")
        }
    }
}
Aswath
  • 1,236
  • 1
  • 14
  • 28
  • I'm no longer using Swift so I can't confirm myself if this works. I'll unmark my own answer as the selected one though. – rmorshea Jul 12 '22 at 18:03
  • @rmorshea Oh, too bad, well I stumbled upon your question in search of an answer for a problem of mine. I got mine resolved, so I thought it might work for your use case as well. – Aswath Jul 13 '22 at 06:33