5

I'm finally making the switch from Objective-C to Swift. I'm creating a view layout system for my client to make their apps more flexible in layout, without using auto layout as they want to design their screens remotely and auto-layout would be too complex for them. I tried to do this using structs and protocols but I found it to be quite clumsy, so I'm suspecting I'm not thinking about it the right way.

With classes, the structure would be as follows:

class ViewModel {
    var frame: CGRect = .zero
}

class ViewGroupModel: ViewModel {
    var weight: Int = 1
    var children:[ViewModel] = [ViewModel]()
}

class HorizontalViewGroupModel: ViewGroupModel {
}

class VerticalViewGroupModel: ViewGroupModel {
}

I tried to approach it with protocols by defining a ViewModel protocol, and a ViewGroupModel protocol, but I found it created a lot of duplication (properties). Is there a better approach? Would it be considered a good practice to use classes in this case?

EDIT: In case it would be better to not uses classes, I am looking for an answer that gives me a concrete solution in terms of structs and protocols.

Joris Weimar
  • 4,783
  • 4
  • 33
  • 53
  • "Would it be considered a good practice to use classes in this case?" - I would say "Yes" because then you'll be able to use ObjectMapper to map JSON objects to your model – mag_zbc Jul 28 '17 at 15:11
  • 1
    Note there is `CGRect` that does already what your model does. – Sulthan Jul 28 '17 at 15:18
  • 6
    @mag_zbc Changing design just because you want to use a library is like the worst reason of all. – Sulthan Jul 28 '17 at 15:19
  • 3
    ... esp since Swift 4 largely obsoletes that library. – Rob Jul 28 '17 at 15:20
  • 5
    ... and JSON data processing is not mentioned at all in the question. – vadian Jul 28 '17 at 15:23
  • I would use JSON objects, but I have to agree with @Sulthan about not letting my design depend on the use of a library :) – Joris Weimar Jul 28 '17 at 15:47
  • 1
    ...and because it's not like ObjectMapper is the only way to deal with JSON. – Tom Harrington Jul 28 '17 at 17:40
  • Favor Structs over classes as you want as little mutability in your code as possible: http://alisoftware.github.io/swift/2015/10/03/thinking-in-swift-3/ – Shaun Jul 28 '17 at 17:54

3 Answers3

6

If the concern was merely how to implement the protocol's properties, I wouldn't necessarily let that sway my choice between struct vs class. If you had a variety of properties that your struct types must implement, you have two basic options:

  1. If you're talking about a few properties, just implement those few properties in your struct types that conform to that protocol. We do this all the time. E.g. when defining custom types that conform to MKAnnotation, we simply implement the three required properties.

    Sure, if we're talking about a much larger set of properties, this gets tedious, but the compiler holds our hand through this process, ensuring that we don't miss anything. So the challenge is fairly modest.

  2. While I'm not a fan of this approach, https://stackoverflow.com/a/38885813/1271826 shows that you could implement the shared properties as a component, where you have struct to wrap all of these properties, and then implement default computed properties for your protocol in an extension:

    enum SubviewArrangement {
        case none
        case horizontal
        case vertical
        case flow
    }
    
    struct ViewComponent {
        var frame = CGRect.zero
        var weight = 1
        var subviews = [ViewModel]()
        var subviewArrangement = SubviewArrangement.none
    }
    
    protocol HasViewComponent {
        var viewComponent: ViewComponent { get set }
    }
    
    protocol ViewModel: HasViewComponent { }
    
    extension ViewModel {
        var frame: CGRect {
            get { return viewComponent.frame }
            set { viewComponent.frame = newValue }
        }
        var weight: Int {
            get { return viewComponent.weight }
            set { viewComponent.weight = newValue }
        }
        var subviews: [ViewModel] {
            get { return viewComponent.subviews }
            set { viewComponent.subviews = newValue }
        }
        var subviewArrangement: SubviewArrangement {
            get { return viewComponent.subviewArrangement }
            set { viewComponent.subviewArrangement = newValue }
        }
    }
    

    Where, you can then create an instance that conforms to ViewModel, like so:

    struct LabelModel: ViewModel {
        var viewComponent = ViewComponent()
    }
    
    var label = LabelModel()
    label.weight = 2
    print(label.weight)
    

    I have to confess, this isn't the most elegant approach. (I hesitate to even present it.) But it avoids having to implement all of those properties individually in your types that conform to ViewModel.

So, let's set the property question aside. The real question is whether you should be using value type (struct) or reference type (class). I think it's illuminating to consider Apple's discussion of value vs reference semantics near the end (@42:15) the Protocol-Oriented Programming in Swift video. They touch upon those cases where you actually may still want to use classes. For example, they suggest you might want to use reference types when, "Copying or comparing instances doesn't make sense". They suggest this rule might apply when dealing with "Window" instances. The same applies here.

On top of that, it doesn't seem to me that there is much benefit to use value types to represent a view hierarchy, which is a collection of reference type objects. It only makes it more confusing. I would just stick with class types, as it will accurately mirror the view hierarchy it represents.

Don't get me wrong: We're so used to using reference types that I think it's always good to challenge our preconceived notions and take a long hard look at whether a value type could better address the situation. In this case, though, I simply wouldn't worry about it and just stick with a class hierarchy that mirrors the hierarchy of those objects you're modeling.


That having been said, the class hierarchy proposed in your question doesn't quite feel right, either. It feels strange that you can actually instantiate a ViewModel to which you can't later add subviews (whereas all UIView objects have subview property). Also, your horizontal and vertical group types don't feel correct either. For example, should it be a single type with some "axis" property, like UIStackView or some other "arrangement" property, to broaden the notion to capture UICollectionView layouts, too?. As you'll see in my ViewComponent example, above, I've flattened this a bit, with these two caveats in mind, but do whatever you see fit.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for your great answer (one of the best I've received). Yes, the structure I lay out in the question does not feel right. I guess the ViewModel not having subviews comes from Android model, where a View does not have subviews. I'm still playing with the model a bit, trying to find one that fits well with all three of the big platforms. But yes, I'm now using a "Collection" class that behaves like you propose. – Joris Weimar Aug 08 '17 at 15:33
4

In general, use a class only if you need the special features of classes, which are:

  • A class can have a superclass and/or a subclass; a struct can't.

  • A class is a reference type, while a struct is a value type.

  • Objective-C can introspect a class (esp. if it derives from NSObject), whereas it cannot even see a struct declared in Swift.

matt
  • 515,959
  • 87
  • 875
  • 1,141
0

Its always good to code to interface/protocol than to classes/structs. This your model wisely. You can make use of generics as well beautifully for this purpose. I hope it would save a lot of variables and duplications for you. For layout purpose in my opinion structs coupled with protocols and generics would make a great design. I don't see for you any need to use classes in your case. Its always good to know in and out of a feature to better use it. The main difference between structs and classes in Swift are

  1. Class objects are stored/passed as references where as struct instances are stored/passed as values
  2. Reference counting allows more than one reference to a class instance.
  3. For Class we have identity operators === and !== which are used to check whether two variables or constants refer to the same instance of Class. The question for identity operators does not arise for Struct because two different Struct variables or constants can not point to same instance. You can try applying identity operator to Struct types. You will get compile time error.
  4. Inheritance enables one class to inherit the characteristics of another.
  5. Type casting enables you to check and interpret the type of a class instance at runtime.
  6. Deinitializers enable an instance of a class to free up any resources it has assigned.

For detailed disscussion you can through my post Struct Vs Classes in Swift.

Mohammad Sadiq
  • 5,070
  • 28
  • 29