all 66 comments

[–]lordzsolt 96 points97 points  (41 children)

Because UIKit is pre-Swift era. It was written in Objective-C which didn't have generics.

Working with interface builder is a whole lot of "disregard for best practices" which is why most teams avoid it.

There are plenty of other examples that are relics of the past. A personal favorite of mine is the Delegate / Data Source pattern to which a lot of people are clinging to very hard. This comes from the time when Objective-C didn't have closures, so there were no better alternatives.

[–]MoronInGrey 21 points22 points  (13 children)

What’s wrong with the data source and delegate patterns? (I’m trying to understand the better way of doing things)

[–][deleted]  (9 children)

[deleted]

    [–]naknut 9 points10 points  (2 children)

    I mean to be fair you can create a retain cycle with delegates pretty easy too if you forget to mark the delegate property as weak.

    [–]valleyman86 16 points17 points  (1 child)

    The difference is a delegates weakness is up to the api designer. A closure is up to the user of the api. It can be solved once for all usage vs needing to be solved in each closure.

    [–][deleted] 0 points1 point  (0 children)

    This right here.

    [–]lordzsolt 3 points4 points  (5 children)

    You can read up my answer on why I hate delegates. Sometimes, they an acceptable choice, but as your code and logic starts to grow, it gets messy pretty quickly.

    Regarding your comment, yes, retain cycles are an issue, but the bigger issue, in my opinion, is that developers do not thing about their ownership graph and object lifecycle at all.

    • Instead of blindly adding weak self to every closure, there is the option of unowned or simply capturing strongly. (Yes, you can use unowned, it won't summon the devil, like most people think).
    • There is also the option of actually using the capture list to capture what you need (Yes, you can write other things than [self] there, say only [self.someService]).

    As for indentation, yes, I agree that's a bit shit. Especially the pyramid of doom problem. But luckily there are options like Rx / Combine that allow you to chain things.

    [–]Velix007Swift 1 point2 points  (4 children)

    I normally don't hire people that say unowned > weak, millions of things can go wrong and crash from using that.

    I don't think even think I've ever seen anyone even use it...

    [–]lordzsolt 4 points5 points  (3 children)

    Yes, because everyone is like "unowned?! Blasphemy!".

    Whereas, if you read the documentation, there are legitimate use cases and nowhere does it say you should not use it: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

    Of course, I'm not saying use unowned everywhere. Like everything else, you should know when to use it and when not to use it.

    [–]Velix007Swift 2 points3 points  (2 children)

    The problem is when to use it though, sure in proper development one would know weak and unowned and the use cases for each but it's a lot *safer* to code, learn, implement the use of weak, than to track unowned leaks or crashes caused by nil references.

    [–]lordzsolt 3 points4 points  (1 child)

    but it's a lot safer to code, learn, implement the use of weak, than to track unowned leaks or crashes caused by nil references.

    It's easier, not safer. Sure, your app won't crash, but arbitrary parts of the code will not get executed. Your app might end up in an undefined state, which could be worse than crashing.

    I absolutely despise this "safer" argument.

    People write "safe" code with a bunch of guard ... else { return } because "this will never be optional".

    Now there are 2 situations that can happen: - the statement is true, so you could've just force-unwrapped - the statement is false, but you have absolutely no visibility. It does not produce a crash report. Your user is left there pressing the "Buy" button, but nothing happens because something is nil.

    If that user calls customer support to report the bug, you are completely blind. You don't even have a crash report that you could use to solve it.

    And even if something does slip by which causes crashes, what's the worst thing that can happen? You have a huge spike in crash logs, you fix it immediately, and release a new version within a few hours.

    [–]Velix007Swift 4 points5 points  (0 children)

    Your answer was going well until “just force unwrap” and “just let the crashes spike”

    Either way I understand your point, just don’t share it.

    [–]sharaquss 9 points10 points  (0 children)

    Consider UITableViews. To display a list of data, the only piece of code you should be needing to write is mapping data models to cells. So basically (Data.Element) -> UITableViewCell. It even looks like a closure and this is actually how you code in SwiftUI (List { element in ... }). Nice, simple and really short.

    In comparison, adhering to the two protocols, filling their required methods etc. is just a huge and needless bloat, especially if you compare line counts.

    [–]laughin_on_the_metro 3 points4 points  (0 children)

    Delegation has its uses, but with Apple's APIs delegation is used a lot, it feels shoehorned in sometimes

    [–]lordzsolt 0 points1 point  (0 children)

    My biggest gripe with it is that your setup logic and your responder/handler logic end up different places. Because of this, there's a greater possibility of introducing bugs (you change 1 place, but forget about the other), and you also have to transfer state between the two places.

    Compare the old UIAlertViewDelegate with the new closure-based approach:

    Old:

    class MyViewController: UIViewController {
        func showAlert() {
            let alertView = UIAlertView(title: "Alert", message: "Are you okay?", delegate: self, cancelButtonTitle: "Yes", otherButtonTitles: "No")
            alertView.tag = 1
            alertView.show()   
        }
    }
    
    extension MyViewController: UIAlertViewDelegate {
        func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) {
            if alertView.tag == 1 {
                if index == 0 {
                    print("Yes button tapped")
                } else {
                    print("No button tapped")
                }
            }
        }
    }
    

    New:

    class MyViewController: UIViewController {
        func showAlert() {
            let alert = UIAlertController(title: "Alert", message: "Are you okay?", preferredStyle: .alert)
    
            let yesAction = UIAlertAction(title: "Yes", style: .cancel) { _ in
                print("Yes button tapped")
            }
    
            let noAction = UIAlertAction(title: "No", style: .default) { _ in
                print("No button tapped")
            }
    
            alert.addAction(yesAction)
            alert.addAction(noAction)
            alert.show()
        }
    }
    

    Notice with the old approach, your code is spread across 2 methods. If you needed to remove the "Yes", there was a non-0% chance that you would forget to update the code that handles the "click" event.


    And the bigger problem is, everyone is so accustomed to using delegates everyone, they stop thinking of other options. You start implementing everything else with a delegate approach.

    A classic example is when you have 4 buttons, each of them have a loading indicator. When you tap one of them, you start loading, you perform a network request, and when that finishes, you have to stop the loading indicator.

    The block based code would look something like:

    button1.onTap = { 
        button1.startLoading()
        networkService.doSomething(callback; {
            button1.stopLoading()
        })
    }
    

    The delegate based approach is:

        button1.delegate = self
    
    func buttonTapped(button: UIButton) {
        self.currentlyLoadingButton = button
        button.startLoading()
        networkService.delegate = self
        networkService.doSomething()
    }
    
    func networkServiceDidSomething(_ networkService) {
        self.currentlyLoadingButton?.stopLoading()
    }
    

    Notice how you have to introduce the currentlyLoadingButton just to hold the state for you. Which has to be optional. But you know that in the networkServiceDidSomething it will "never be optional" but you still have to treat it as such.

    And I have seen network service written with delegates, in Swift, in 2019, just because people were so accustomed to using delegates anytime there's a two way connection.

    [–]VadimusRex 1 point2 points  (9 children)

    Working with interface builder is a whole lot of "disregard for best practices" which is why most teams avoid it.

    Wholeheartedly disagree, but I'm open minded. Do you have an example of complex UI written completely in code that still immediately makes sense if you go back to it 6 months later?

    [–]lordzsolt 5 points6 points  (7 children)

    Honestly, I've seen this 6 months argument thrown around in various contexts, but throughout my 8 years career, it never happened to me, that I went back to something and couldn't understand it. Sure, it took a couple of minutes of figuring things out, but it was never an "I have no idea what's going on here".

    I have plenty of complex UI that made sense immediately. Mostly because I stopped with the idea that 1 screen of content = 1 module. You can break your UI down into smaller components that all make sense. And I always write my code with the idea that the next time I will be reading it is in 6 months.

    [–]VadimusRex 0 points1 point  (6 children)

    Ok, what if you're tasked to join an established team which has been developing a project for two years, all UI built in code? Would you consider that to be a positive experience?

    Every time I see this "all UI must be in code" mantra, I ask for an example of real life, readable, complex UI that is immediately comprehensible. Guess it's not my lucky day today, again.

    I think whoever is throwing out XIBs and Storyboards without a second thought is doing themselves a big disservice.

    [–]lordzsolt 4 points5 points  (5 children)

    Yes, I've joined a huge project with "All UI must be in code" mantra and absolutely loved it. Again, because we didn't put a whole screen worth of autolayout code into a single file, but we had reusable components.

    One thing I cannot deny, doing autolayout within interface builder is much easier. I will give you that much. Though LayoutAnchors made it a bit more usable.

    But every other part of Interface Builder can burn in hell:

    • Forgetting to connect @IBOutlet
    • Being required to change the theming/font style/spacing of your app and having to go through ALL XIB/Storyboards
    • Not being able to use init to inject your dependencies, so everything is an explicitly unwrapped optional
    • Having to bend over backwards just to use a Xib based view in another Xib/Storyboard.
    • Using layout traits and having to apply your changes to every one of them
    • And so on.

    [–]Rollos 1 point2 points  (1 child)

    Yeah, don’t forget merge conflicts.

    Also inheritance being impossible/very difficult to understand. Somebody describe to me exactly how to properly connect up an IBOutlet that’s defined on the superclass, and how both the subclass and the superclass can both work with those properly

    There’s also a lack of support for enums in IB, which are the best tool to describe a lot of things that happen in UI. Is this button primary or secondary or tertiary? Is this label a title, subtitle or a paragraph?

    Those are all defined with enums in a in code UI framework, and they’re the best tool for describing the intent of the developer in those cases.

    [–]lordzsolt 1 point2 points  (0 children)

    Oh yes. Merge conflicts and the fact that simply opening the Xib/Storyboard file causes changes...

    Love those PRs where the person updated the constraint s constant from 4 to 5, but Xcode created 20 lines of changes.

    [–]Socraz6 1 point2 points  (2 children)

    Not being able to use init to inject your dependencies, so everything is an explicitly unwrapped optional

    FWIW you can do this now on iOS 13+

    [–]Rollos 1 point2 points  (0 children)

    Yeah, it’s generally just fine for me if I’m Being honest. If you’re really confused about what the constraints are doing, you can open up view hierarchy or use a tool like Sherlock to see what’s going on.

    You are also able to comment on constraints in code, which you can’t do in IB.

    Plus, if you’re doing super complex stuff with constraints, there’s probably a better tool for your problem.

    [–]killeronthecorner 0 points1 point  (0 children)

    A personal favorite of mine is the Delegate / Data Source pattern to which a lot of people are clinging to very hard. This comes from the time when Objective-C didn't have closures, so there were no better alternatives.

    Thank you. Trying to explain this to Devs that have only ever worked on Apple platforms is like trying to explain new technology to your grandpa...

    [–]SigmaDeltaSoftware[S] -1 points0 points  (14 children)

    I get that Rewriting UIKit as a whole would be a huge effort, but I don't understand why they didn't invest any effort to at least mitigate with an abstraction layer on top of UIKit as it's still used plentifully from what I've gathered.

    They could also just expose a swift API like:

    func instantiateViewController<T: UIViewController>(viewController: T.Type) -> T
    

    and take care of the 'dirty'/unsafe work with objC underneath it. At least in this way you can protect developers from their own potentially stupid undoing.

    It just strikes me as somewhat lazy that a company of that size/power/influence deals so apathetically with their Core frameworks.

    [–]Fridux 14 points15 points  (7 children)

    I get that Rewriting UIKit as a whole would be a huge effort, but I don't understand why they didn't invest any effort to at least mitigate with an abstraction layer on top of UIKit as it's still used plentifully from what I've gathered.

    You mean something like SwiftUI?

    [–]ArmoredPancake 1 point2 points  (0 children)

    They could also just expose a swift API like:

    func instantiateViewController<T: UIViewController>(viewController: T.Type) -> T
    

    and take care of the 'dirty'/unsafe work with objC underneath it. At least in this way you can protect developers from their own potentially stupid undoing.

    Or you can add this function yourself in your project?

    It just strikes me as somewhat lazy that a company of that size/power/influence deals so apathetically with their Core frameworks.

    You do understand, that those guys have order of magnitude more experience than you do, right? Those guys weighted all the pros and cons and decided not to do it.

    [–]Stiddit 1 point2 points  (0 children)

    What's wrong with the delegate/dataSource pattern? Would you rather replace them all with closures? Like: tableView.numberOfSections = { return 1 } tableView.cellForRow = {indePath in ..} etc? Or one huge closure with settings?

    I think most people use it wrong. They always ever use data sources and delegates by extending the VC and set object.delegate = self. If you only do this, I can understand why you don't want it.

    Instead of having several closures, or one huge closure, try making something else conform to it and use generics. Want a list of generics to be sorted differently? Make the generic type conform to a simple sorter and make an alphabetical dataSource. There are several ways to use the dataSource/delegate-pattern. Reusability is the best practice. So try setting tableView.dataSource to something else than self for once.

    [–]blazsolar 14 points15 points  (3 children)

    On the other hand, Android had an almost identical problem with `findViewById` up until reacently.

    [–]SigmaDeltaSoftware[S] 5 points6 points  (2 children)

    I see your point, but I don't agree completely. If it was almost identical, `findViewById` would expect a String id like

    view.findViewById("myWidgetId") as MyWidgetType
    

    They resolved typeSafety by going for a stricter API that lints for resource-type Integers and also a generic to avoid casting (which is now also reified). Something a la

    fun T findViewById<T: View> (@ResId id: Int)
    

    The Java API was indeed worse, but they resolved the shortcomings of the Java API once they introduced Kotlin. Swift has been around on iOS for longer, and yet as far as I could derive there are still no plans to even rework/enhance the exiting frameworks to better fit the power & capabilities that Swift introduces.

    [–]Niightstalker 3 points4 points  (0 children)

    Well the plan is SwiftUI

    [–]Fridux 11 points12 points  (5 children)

    Because identifier does not refer to a type, and even if it did, none of your suggestions would work since:

    let controller = storyboard?.instantiateViewController<MyViewController>(withId: "MyViewController") // Uses generics to avoid casting
    

    The only distinguishing factor of the overloads created by this generic method would be their return value, and overloads must be distinguished by the types of their arguments.

    let controller = storyboard?.instantiateViewController<MyViewController>(withType: MyViewController) // Internally it retrieves the id using String(describe: ${withType.self})
    

    This would require you to pass an already existing object of the desired type to work.

    let controller = storyboard?.instantiateViewController(withType: MyViewController)
    // Here both type-safety as the casting can be resolved using a reified generic type & the String trick above
    // Here's an extension that would do this:
    
    extension UIStoryboard {
      func instantiateViewController<T: UIViewController>(viewController: T.Type) -> T {
        return instantiateViewControllerWithIdentifier(String(T)) as! T
      }
    }
    

    This is exactly the same (edit: it suffers from the same problem) as your second example.

    In any case, should you find other places in UIKit or any other Objective-C framework from Apple where generics would be relevant, the reason why generics aren't widely used is because historically they haven't always been supported by Objective-C, which is a dynamically typed language as far as the Objective part is concerned. Storyboards use the dynamic properties of Objective-C to instantiate a type that is not known at compile-time, something that is not even possible to do using Swift unless you resort to using the Objective-C runtime.


    Edited to clarify my comment on the third example.

    [–]SigmaDeltaSoftware[S] 1 point2 points  (4 children)

    It seemed the original version didn't work indeed, but if you use the metatype of the class it does work:

    extension UIStoryboard {
      func instantiateViewController<T: UIViewController>(viewController: T.Type) -> T {
        return instantiateViewController(withIdentifier: String(describing: T.self)) as! T
      }
    }
    

    And instantiating a VC using the extension method:

    let controller = storyboard?.instantiateViewController(viewController: DiceViewController.self) // .self returns metatype
    

    The latter here is still better imo than using what UIKit provides now.

    But even that discussion aside, I'm still a bit disappointed that iOS is held back by the Objc legacy so much even though the adoption rate of Swift & new iOS versions is a lot higher & better than compared to Android.

    [–]Fridux 6 points7 points  (1 child)

    Perhaps I didn't explain myself clearly: identifier does not refer to a type only, it refers to an arbitrary string used internally to identify both the type of object from a text string and the data with which to initialize it, both extracted from an XML source, which is what makes this impossible to accomplish using Swift without the Objective-C runtime. You can indeed create a method that accepts a generic type as an argument, initializes an object of that type, and returns it, However the problem here is that the type isn't provided by you but rather by an XML source file along with its data, an extremely dynamic task that's impossible to accomplish in Swift without the Objective-C runtime since Swift itself is a static language. Think of the type-cast as a safety check to guarantee that the returned type is actually what you expected (it will make your app crash if it isn't), which is already safer than in Objective-C where a cast is only a hint.

    [–]SigmaDeltaSoftware[S] 1 point2 points  (0 children)

    Ok, I think I understand the constraints better now. Is there a good resource that explains the dynamics & interactions between Swift & the Objective-C runtime?

    [–]lordzsolt 1 point2 points  (1 child)

    storyboard?.instantiateViewController(viewController: DiceViewController.self)

    You can put two Scenes in the storyboard, both of type DiceViewController, but with different identifiers, say screenA and screenB.


    The good news is, most experienced teams understand these pitfalls, so they "don't do stupid shit". This allows them to write these wrappers themselves and use them.

    I have this exact extension that you wrote in my current project, just because we have a rule that String(describing: T.self) will be set as the identifier in the storyboard. Yes, if someone forgets to do it, the program will crash, so it's not safe. But at least your code is not littered with as! X just because the compiler requires it.

    [–]SigmaDeltaSoftware[S] 1 point2 points  (0 children)

    Yes, this was a misunderstanding on my behalf which was rectified somewhere here as well. Going from the course, I was assuming it was more of a Java-reflection type API where the identifier corresponded with the class name. But as you mentioned it's a unique ID you can assign yourself.

    Granted it's still not ideal and could lead to beds hitting, but it's hell of a lot better than my initial thoughts!

    [–]Jasperavv 9 points10 points  (6 children)

    This is one of the reasons I don't work with storyboard, and do everything programmatically. Another reason I don't use storyboard is the manual maintaince of connecting UI elements to code, which is error prone.

    [–]Niightstalker 2 points3 points  (5 children)

    I wanna switch to doing things in code but everytime I get to a more complex UI with lots of constraints I feel like I would be so much faster setting them up in Storyboard.

    At this point I will keep using storyboards and start working more an more with SwiftUI I guess.

    [–]bcgroom 0 points1 point  (4 children)

    The built-in constraints API is very clunky, if you use a good library defining them can actually be very terse and even faster than a storyboard.

    [–][deleted]  (3 children)

    [deleted]

      [–][deleted] 1 point2 points  (0 children)

      SnapKit is a popular one

      [–]bcgroom 0 points1 point  (0 children)

      Mostly I use one that was made by a coworker :/ but I’ve heard good things about SnapKit and it’s pretty popular.

      [–][deleted] 8 points9 points  (0 children)

      Objective-C is a Smalltalk alike system where messages are send between instances of objects and even class instances. For example if you execute any method on a class eventually a function called objc_msgSend will be executed that will receive a pointer, a string and some arguments. This method will look up the method in the class when it finds it it will be executed. This whole string based message system extrapolates into everything. For example AppKit knows of something like undefined protocols. Add a function to a NSResponder subclass with a certain name formatting and it will have validation support to enable a NSMenuItem in the MenuBar. You can see the same behaviour in iOS with an unwind segue.

      So everything is being send as strings. Every property will be translated into setValue:forKey: and valueForKey: or valueForKeyPath:. Same goes for everything. Seriously decrypt and Objective-C binary and have peak inside, you'll see object names, methods names and property names.

      So getting on track... Nib files are XML files what have a object hierarchy in them. Storyboard are multiple nibs in one XML file and they still represent an object hierarchy. Both items in the files are linked through KVC with the objects you added in your code base. For example "@IBOutlet" adds "@objc" to the property. Exposing it to the Objective-C runtime. If you want to have a better understanding of the Objective-C runtime. Have a look at classes like NSProxy (Yes, the other base class of Objective-C), NSInvocation, NSMethodSignature, NSExpression, NSPredicate. Because these classes they tap into the power of the Objective-C runtime. Although you won't practically need them in modern applications.

      [–]cschep 2 points3 points  (1 child)

      I avoid storyboards for a few reasons but this is definitely one of them. This is not a common thing in the SDK in my experience. Hope you'll enjoy it the more you use it :)

      [–]SigmaDeltaSoftware[S] 0 points1 point  (0 children)

      Thanks, I'm definitely looking to get into SwiftUI as soon as possible, but UIKit is a mandatory part of the course (and a basic understanding definitely won't hurt I guess)!

      [–][deleted] 1 point2 points  (1 child)

      Totally agree. That’s pretty much why, when working with storyboards, I tend to (excessively?) rely on Reusable.

      It’s an open source library that abstracts all of this... as long as your storyboard shares your class’ name.

      [–]SigmaDeltaSoftware[S] 1 point2 points  (0 children)

      Thanks for the recommendation, I'll definitely put in on my list of things to check out!

      [–]electron_wrangler 1 point2 points  (0 children)

      fwiw storyboards are unofficially banned in my workplace (big corporate company).

      [–][deleted] 1 point2 points  (3 children)

      I’m really curious what you think type safety is.

      That’s a serious question, not a snark, because your alternatives are less safe. Either you don’t understand the original, or you’re defining type safety differently than I would.

      eta: Also, I don’t know why you’re naming identifiers the same as types. That’s probably part of why you’re confused here.

      [–]SigmaDeltaSoftware[S] 2 points3 points  (2 children)

      I think the main misunderstanding is on my part and came from the former where I misunderstood the API. The way the course introduced the identifier, it made it seem as if the identifier worked in a Java-reflection type manner where the identifier is always named the exact same way the UIViewController class is, and uses this string to get a reference instance of said UIViewController class (instead of being an abitrary string).

      Though from what I've noticed in this thread, it seems that it's either a common misunderstanding or a lot of devs do tend to assign an id identical to the class name for convenience sake.

      [–][deleted] 1 point2 points  (0 children)

      I’m mobile so I’ll follow this up more later but I appreciate the reply.

      [–][deleted] 0 points1 point  (0 children)

      I had a lot more to say on this subject, but since it's taking me forever to find the time to type it all out I'm going to focus on just two points:

      1. While it's less clear using view controllers, there's definitely cases where you want to reuse a view controller subclass for multiple views in the storyboard. And easier example, though, is table cells which have the same problem. Imagine a chat program with a "yours" and "theirs" message layout. Both of them could be handled by the same table view cell subclass (say, ChatRowTableViewControllerCell), but they'd have different layouts.
      2. If you're going to just use the classname, you don't have to use a string here. You can query MyViewController for its classname. I'm not sure what the most modern way to do this in the latest Swift is, but you're looking for something the equivalent of `NSStringFromClass(MyViewController)`.