Read-only and non-computed variable properties in Swift

Objective CSwiftProperties

Objective C Problem Overview


I'm trying to figure out something with the new Apple Swift language. Let's say I used to do something like the following in Objective-C. I have readonly properties, and they cannot be individually changed. However, using a specific method, the properties are changed in a logical way.

I take the following example, a very simple clock. I would write this in Objective-C.

@interface Clock : NSObject

@property (readonly) NSUInteger hours;
@property (readonly) NSUInteger minutes;
@property (readonly) NSUInteger seconds;

- (void)incrementSeconds;

@end

@implementation Clock

- (void)incrementSeconds {
     _seconds++;

     if (_seconds == 60) {
        _seconds = 0;
        _minutes++;
    
         if (_minutes == 60) {
            _minutes = 0;
            _hours++;
        }
    }
}

@end

For a specific purpose, we cannot touch the seconds, minutes and hours directly, and it's only allowed to increment second by second using a method. Only this method could change the values by using the trick of the instance variables.

Since there are no such things in Swift, I'm trying to find an equivalent. If I do this:

class Clock : NSObject {

    var hours:   UInt = 0
    var minutes: UInt = 0
    var seconds: UInt = 0

    func incrementSeconds() {

        self.seconds++

        if self.seconds == 60 {

            self.seconds = 0
            self.minutes++

            if self.minutes == 60 {

                self.minutes = 0
                self.hours++
            }
        }
    }
}

That would work, but anybody could change directly the properties.

Maybe I already had a bad design in Objective-C and that's why the potential new Swift equivalent is not making sense. If not and if someone have an answer, I would be very grateful ;)

Maybe the future Access Control Mechanisms promised by Apple is the answer?

Thanks!

Objective C Solutions


Solution 1 - Objective C

Simply prefix the property declaration with private(set), like so:

public private(set) var hours:   UInt = 0
public private(set) var minutes: UInt = 0
public private(set) var seconds: UInt = 0

private keeps it local to a source file, while internal keeps it local to the module/project.

private(set) creates a read-only property, while private sets both, set and get to private.

Solution 2 - Objective C

There are two basic ways of doing what you want. First way is by having a private property and and a public computed property which returns that property:

public class Clock {

  private var _hours = 0

  public var hours: UInt {
    return _hours
  }

}

But this can be achieved in a different, shorter way, as stated in the section "Access Control" of the "The Swift Programming Language" book:

public class Clock {

    public private(set) var hours = 0

}

As a side note, you MUST provide a public initialiser when declaring a public type. Even if you provide default values to all properties, init() must be defined explicitly public:

public class Clock {

    public private(set) var hours = 0

    public init() {
      hours = 0
    }
    // or simply `public init() {}`

}

This is also explained in the same section of the book, when talking about default initialisers.

Solution 3 - Objective C

Since there are no access controls (meaning that you can't make an access contract that differs depending on who the caller is), here's what I would do for now:

class Clock {
    struct Counter {
        var hours = 0;
        var minutes = 0;
        var seconds = 0;
        mutating func inc () {
            if ++seconds >= 60 {
                seconds = 0
                if ++minutes >= 60 {
                    minutes = 0
                    ++hours
                }
            }
        }
    }
    var counter = Counter()
    var hours : Int { return counter.hours }
    var minutes : Int { return counter.minutes }
    var seconds : Int { return counter.seconds }
    func incrementTime () { self.counter.inc() }
}

This merely adds a level of indirection, as it were, to direct access to the counter; another class can make a Clock and then access its counter directly. But the idea — i.e., the contract we're trying to make — is that another class should use the Clock's top-level properties and methods only. We can't enforce that contract, but actually it was pretty much impossible to enforce in Objective-C too.

Solution 4 - Objective C

Actually access control (which does not exist yet in Swift) is not as enforced as you may think in Objective C. People can modify your readonly variables directly, if they really want to. They just do not do it with the public interface of the class.

You can do something similar in Swift (cut&paste of your code, plus some modifications, I did not test it):

class Clock : NSObject {

    var _hours:   UInt = 0
    var _minutes: UInt = 0
    var _seconds: UInt = 0

    var hours: UInt {
    get {
      return _hours
    }
    }
    
    var minutes: UInt {
    get {
      return _minutes
    }
    }
    
    var seconds: UInt  {
    get {
      return _seconds
    }
    }

    func incrementSeconds() {

        self._seconds++

        if self._seconds == 60 {

            self._seconds = 0
            self._minutes++

            if self._minutes == 60 {

                self._minutes = 0
                self._hours++
            }
        }
    }
}

which is the same as what you have in Objective C except that the actual stored properties are visible in the public interface.

In swift you can also do something more interesting, which you can also do in Objective C, but it's probably prettier in swift (edited in the browser, I did not test it):

class Clock : NSObject {

    var hours: UInt = 0
    
    var minutes: UInt {
    didSet {
      hours += minutes / 60
      minutes %= 60
    }
    }
    
    var seconds: UInt  {
    didSet {
      minutes += seconds / 60
      seconds %= 60
    }
    }

    // you do not really need this any more
    func incrementSeconds() {
        seconds++
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionZaxonovView Question on Stackoverflow
Solution 1 - Objective CEthanView Answer on Stackoverflow
Solution 2 - Objective CelitalonView Answer on Stackoverflow
Solution 3 - Objective CmattView Answer on Stackoverflow
Solution 4 - Objective CAnalog FileView Answer on Stackoverflow