iOS 15 Programming Fundamentals with Swift

suojae·2023년 12월 26일
0

목록 보기
4/8

Preface

Swift has these salient features.

  • Object-orientation
    ”Everything is an object”
  • Clarity
    Swift’s syntax is clear with few hidden shortcuts and minimal syntactic trickery.
  • Safety
    Swift enforces strong typing to ensure that it knows, what the type of every object reference is at every moment.
  • Economy
    Swift is a fairly small language. The rest must be provided by your code or by libraries of code that you use - Such as Cocoa.
  • Memory management
    Swift manages memory automatically.
  • Cocoa compatibility
    Swift is explicitly designed to interface with most of the Cocoa APIs which are written primarily in C and Objective-C.


Chapter01. The Architecture of Swift


Ground of Being

  • A complete Swift command is a statement.
  • Swift is a compiled language. This means your code must build - passing through compiler.
  • The Swift compiler is very strict. The strictness of the compiler is one of Swift’s greatest strengths.

Everything Is an Object?

  • Object: It is something you can send a message to.
  • Message: It is an imperative instruction.

In Swift, the syntax of message-sending is dot-notation. We start with the object; then there’s a dot; then there’s the message.

The idea of everything being an object is a way of suggesting that even “primitive” linguistic entities can be sent message.
In Swift, every noun is an object, and every verb is a message.

An object type can be extended in Swift meaning that you can define your own messages on that type.
In Swift, there are no scalars; all types are ultimately object types. That’s what “everything is an object” really means.


Three Flavors of Object Type

Swift has three kinds of object type: classes, structs, and enums.


Variables

  • A variable is a name for an object. Technically it refers to an object; it is an object reference.
  • The object to which the variable refers is the variable’s value.
  • No variables comes implicitly into existence; all variables must be declared.
  • In Swift declaration is usually accompanied by initialization.
  • A variable declared with let is a constant; its value is assigned once and stays.
  • Variables also have a type. This type is established when the variable is declared and can never change.
  • Variables literally have a life of their own - more accurately, a lifetime of their own.
  • Thus a variable can be not only a way of conveniently naming something, but also a way of preserving it.

Functions

  • In general, executable code must live inside the body of function.
  • A function is a batch of code that can be told, as a batch, to run.
  • go() cannot live on its own either. It might live in the body of a different function
  • Swift also has a special rule that a file called main.swift, exceptionally, can gave executable code at its top level, outside any function body, and this is the code that actually runs when the program runs.

The Structure of a Swift File

  • Module import statements
    • A module is an even higher-level unit than a file.
    • a module can consist of multiple files, and these can all see each other automatically.
    • But a module can’t see another module without an import statement.
  • Variable declarations
    • A variable declared at the top level of a file is a global variable.
    • All code in any file will be able to see and access it.
  • Function declarations
    • A function declared at the top level of a file is a global function.
  • Object type declarations
    • The declaration for a class, a struct, or an enum.

Scope and Lifetime

In a Swift program, things have a scope.
This refers to their ability to be seen by other things.

  • The rule is that things can see things at their own level and at a higher level containing them.
    • A module is a scope.
    • A file is a scope.
    • Curly braces are a scope.
  • When something is declared, it is declared at some level within that hierarchy.
  • Scope is thus a very important way of sharing information.
  • Things also have a lifetime , which is effectively equivalent to their scope.

Namespaces

//Manny is a nameSpace
class Manny {
	class Klass {}
 }

A namespace is a named region of a program. Clearly, namespaces and scopes are closely related notion.
In effect, message-sending allows you to see into scopes you can’t see into otherwise.


Modules

The top-level namespaces are modules.
Your app is a module and hence a namespace.
That namespace’s name is, by default, the name of the app.

  • When you import a module, all the top-level declarations of that module become visible to your code as well.
  • Swift itself is defined in a module - the Swift module. But you don’t have to import it, because your code always implicitly imports the Swift module.
  • print is in fact a function declared at the top level of the Swift module, and your code can see the Swift module’s top-level declarations because it imports the Swift.
  • Your own app module, however , overshadows any modules you import.
  • That means that if you declare a term identical to an imported term, you lose the magical ability to use the imported term without specifying the namespace.
  • If you were to declare a print function of your own, it would effectively hide the Swift print function.
  • You can still call the Swift print function, but now you have to use the namespaced Swift.print explicitly.

Instances

Dog.bark() //compile error

By default, properties and methods are instance properties and methods. You can’t use them as messages to the object type itself. You have to have instance to send those messages to.


Why Instances

  • An instance is responsible not only for the values but also for the lifetimes of its properties.
  • In short, an instance is both code and data.
  • The code it gets from its type and in a sense is shared with all other instance of that type, but the data belong to it alone.
  • An instance is a device for maintaining state. It’s a box for storage of data.

The Keyword self

An instance is an object, and an object is the recipient of messages.
Thus, an instance needs a way of sending message to itself.
This made possible by the keyword self.



Chapter 02. Functions


Variadic Parameters

func sayString(_ array: String ...) {
		for s in array { print(s) }
}

sayStrings("hey", "ho", "nonny nonny")

A parameter can be variadic.
This means that the caller can supply as many argument values of this parameter’s type as desired.


Modifiable Parameters

func removeCharacter(_ c: Character, from s: inout String) -> Int {
		var s = s
		var howMany = 0
		while let ix = s.firstIndex(of:c) {
				s.remove(at:ix)
				howMany += 1
		}
		return howMany
}

let s = "hello"
let result = removeCharacter("1", from: &s) //2
  • The type of the parameter we intend to modify must be declared inout keyword.
  • When we call the function, the variable holding the value to be modified must be declared with var, not let.
  • Instead of passing the variable as an argument, we must pass its address. This is done by preceding its name with an ampersand(&).
  • When a function with an inout parameter is called, the variable whose address was passed was passed an argument to that parameter is always set.

Calling Objectvie-C with Modifiable Parameters

func getRed(_ red: UnsafeMutablePointer<CGFloat>,
    green: UnsafeMutablePointer<CGFloat>,
    blue: UnsafeMutablePointer<CGFloat>,
    alpha: UnsafeMutablePointer<CGFloat>) -> Bool

The Cocoa APIs are written in C and Objective-C.
So instead of the Swift term inout, it use such as UnsafeMutablePointer.


Called by Objective-C with Modifiable Parameters

func popoverPresentationController(
    _ popoverPresentationController: UIPopoverPresentationController,
    willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>,
    in view: AutoreleasingUnsafeMutablePointer<UIView>) {
        view.pointee = self.button2
        rect.pointee = self.button2.bounds
}

If you want to change its value in Cocoa framework, sometimes you have to declare UnsafeMutablePointer not the inout keyword.



Chapter 4. Object types


Intitializers

An ititializer is a function for producing an instance of an object type. Strictly speaking, it is a static/class method, because it is called by talking to the object type.

Observe that Dog() calls an initializer even though our Dog vlass doesn't declare any initializers! The reason is that object types may have implicit initializers.


Properties

A stored instance proprty must be given an initial value. As I explained a moment ago, this doesn't have to happen throught assignment in the declaration.

struct Greeting {
    static let friendly = "hello there"
    static
}

A static/class property is accessed through the type, and is scoped to the type, which usually means that it is global and unique.


Property initialization and self

class Moi {
    let first = "Matt"
    let last = "Neuburg"
    let whole = self.first + " " + self.last // compile error
}

A propert declaration that assigns an initial value to the property cannot fetch an instance property or call an instance method. There is no self yet - self is exactly what we are in the process of initializing.

//Solution1
class Moi {
    let first = "Matt"
    let last = "Neuburg"
    var whole : String {
        self.first + " " + self.last
    }
}

//Solution2
class Moi {
    let first = "Matt"
    let last = "Neuburg"
    lazy var whole = self.first + " " + self.last
}

There are two common solutions in that situation

Make this a cimputed property:
A computed property can refer to self in a getter or setter function because the function won't run until the property is accessed.

Declare this property lazy:
A lazy property can refer to self in its initializer because the initializer won't be evaluated until the property is accessed.

struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static let ambivalent = friendly + " but " + hostile
}

Unlike Instacne properties , static properties can be initialized with reference to one another. the reason is that static property initializers are lazy.

class MyClass {
    var s = ""
    func store(_ s:String) {
        self.s = s
    }
}
let m = MyClass()
let f = MyClass.store(m) // what just happened!?


f("howdy")
print(m.s) // howdy

Instance methods are actually static/class methods. The reason is that an instance method is actually a curried static/class method composed of two functions - one function that takes an instance, and another function that takes the parameters of the instance method.


Enum Properties

enum Filter : String {
    case albums = "Albums"
    case playlists = "Playlists"
    case podcasts = "Podcasts"
    case books = "Audiobooks"
    var query : MPMediaQuery {
        switch self {
        case .albums:
            return .albums()
        case .playlists:
            return .playlists()
        case .podcasts:
            return .podcasts()
        case .books:
            return .audiobooks()
        }
    }
}

An enum can have instance properties and static properties, but there's a limitation: An enum instance properyty can't be a stored property"

enum Silly {
    case one
    var sillyProperty : String {
        get { "Howdy" }
        set {} // do nothing
    }
}

var silly = Silly.one
silly.sillyProperty = "silly"

Code's reference to the enum instance itself must be a variable(var), not a constant(let). In you try to assign to an enum instance property through a let reference to the enum, you'll get a compile error.

enum Filter : String, CaseIterable {
    case albums = "Albums"
    case playlists = "Playlists"
    case podcasts = "Podcasts"
    case books = "Audiobooks"
    static subscript(ix: Int) -> Filter {
        Filter.allCases[ix] // warning, no range checking
    }
}

let type = Filter[2] // podcasts

A subscript can be a static method.


Why Enums?

An enum is a switch whose states have names.

Even when there are only two states, an enum is often better than, say, a mere Bool, because the enum's states have names. Moreover, you can store extra information in an enum's associated value or raw value.

enum InterfaceMode : Int {
    case timed = 0
    case practice = 1
}

var interfaceMode : InterfaceMode = .timed {
    willSet (mode) {
        self.timedPractice?.selectedSegmentIndex = mode.rawValue
    }
}

And what are my interfaceMode enum's raw value integers for? That's the really clever part. They correspond to the segment indexes of a UISegmentedController in the interface!


Struct Methods

A struct can have instance methods and static methods, including subscripts, Inf an instance method sets a property, **it must be marked as mutating, and the callers reference to the struct instance must be a variable(var), not a constant(let).


Class

Class are reference types. This means, among other things, that a class instance has two remarkable features that are not true of struct or enum.

Mutability
Even if your reference to an instance of a classs is a constant(let), you can change the value of an instance property through that reference.

Multiple reference
When a given instance of a class is assigned to multiple variables or passed as argument to a function, you get multiple references to one and the same object.


The keyword super

class Dog : Quadruped {
    func bark () {
        print("woof")
    }
}
class NoisyDog : Dog {
    override func bark () {
        for _ in 1...3 {
            super.bark()
        }
    }
}

let fido = Dog()
fido.bark() // woof
let rover = NoisyDog()
rover.bark() // woof woof woof

It often happens that we want to override something in a subclass and yet access the thing overrideen in the superclass. This is done by sending a message to the keyword super.

NoisyDog really does when it barks is the same thing Dog does when it barks, but more times.


Summary of Type Terminology

type(of:): Applied to an object: the polymorphoc (internal) type of the object, regardless of how a refernce is typed.
Self: In a method body
.Type: Appended to a type in a type declaration to specify that the type itself(or a subtype) is expected.
.self: Sent to a type to generate a metatype, suitable for passing where a type(.Type) is expected.


Why protocols?

Protocol thus give us another way of expressing the notion of type and subtype - and polymorphism applies.

Note that a type cna adopt more than onr protocol.


Protocol Composition

func f(_ x: CustomStringConvertible & CustomDebugStringConvertible) {
}
protocol MyViewProtocol {
    func doSomethingReallyCool()
}
class ViewController: UIViewController {
    var v: (UIView & MyViewProtocol)?
    func test() {
        self.v?.doSomethingReallyCool() // a MyViewProtocol requirement
        self.v?.backgroundColor = .red // a UIView property
    }
}

If the only purpose of a protocol is to combine other protocols by adopting all of them, without adding any new requirements, you can avoid formally declaring the protocl in the first place by specifiying the protocol combination on the fly. To do so, join the protocol names with &.

protocol MyClassProtocol : AnyObject {
    // ...
}

To specify that a protocol can be adopted only by some class (and not a struct or enum) without specifying what class it must be, use the protocol type AnyObject which every class type adopts.

protocol MyViewProtocol where Self:UIView {
    func doSomethingReallyCool()
}
protocol MyClassProtocol where Self:AnyObject {
    // ...
}

A valuable byproduct of declaring a class protocol is that the resulting type can take advantage of special memory management features that apply only to classes.

protocol SecondViewControllerDelegate : AnyObject {
    func accept(data:Any)
}
class SecondViewController : UIViewController {
    weak var delegate : SecondViewControllerDelegate?
    // ...
}

Te keyword weak marks the delegate property as having special memory management that applies only to class instances.

The delegate property is typed as a protocol, and a protocol might be adopted by a struct or an enum type.

So to satisfy the compiler that this object will in fact be a class instance and not a struct or enum instance, the protocol is declared as a class protocol.


Implicitly Required Initializers

protocol Flier {
    init()
}
class Bird : Flier {
    init() {} // compile error
}

protocol Flier {
    init()
}
class Bird : Flier {
    required init() {}
}

Suppose that a protocol declares an initializer. And suppose that a class adopts this protocol. By the terms of this protocol, this class and any subclass it may ever have must implement this initializer. Therefore, the class not only must implement the initializer, but also must mark it as required.

Alternatively, if Bird were marked final, there would be no need to mark its init as required, because this would mean that Bird cannot have any subclasses-guaranteeing that the problem will never arise in the first place.


class ViewController: UIViewController {
    init() {
        super.init(nibName:"ViewController", bundle:nil) // compile error
    }
}

That code won't compile. The compile error says "required initializer init(coder:) must be provided by subclass of UIViewController."

It turns out that UIViewController adopts a protocol called NSCoding. And this protocol requires an initializer init(coder:), which UIViewController duly implements.

UIViewController and NSCoding are declared by Cocoa, not by you. This is the same situation I was just describing. Your UIViewController subclass must either inherit init(coder:) or must explicitly implement it and mark it required.

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

You are being forced to write an initializer for which you can provide no meaningful functionality!. Fortunately, Xcode's fix-it feature will offer to write the initializer for you like above.

If, on the other hand, you do have functionality for this initializer, you will delete the fatalError line and insert that functionality in its place. A minimum meaningful implementation would be to call super.init(coder:coder).

Not only UIViewController but lots of built-in Cocoa classes adopt NSCoding.


Expressisble by Literal

Literals are a case in point. The reason you can say 5 to express an Int whose value is 5, instead of formally initializing Int by saying Int(5), is not because of magic (or at least, not entirely because of magic). It’s because Int adopts a protocol, ExpressibleByIntegerLiteral.

struct Nest : ExpressibleByIntegerLiteral {
    var eggCount : Int = 0
    init() {}
    init(integerLiteral val: Int) {
        self.eggCount = val
    }
}

func reportEggs(_ nest:Nest) {
    print("this nest contains \(nest.eggCount) eggs")
}
reportEggs(4) // this nest contains 4 eggs


Generics

I have already said that an Optional is an enum, with two case .none and .some. If an Optional's case os .some, it has an associated value - the value that is wrapped by this Optional.

enum Optional<Wrapped> : ExpressibleByNilLiteral { 
    case none
    case some(Wrapped) 
    init(_ some: Wrapped) 
    // ...
}

That is the pseudocode declaration of an Optional whose Wrapped placeholder has been replaced everywhere with the String type.


func dogMakerAndNamer(_ whattype:Dog.Type) -> Dog {
    let d = WhatType.init(name:"Fido")
    return d
}

If we are passed a Dog subclass such as NoisyDog as the parameter, we will instantiate that type (which is good) but then return that instance typed as Dog (which is bad).

func dogMakerAndNamer<WhatType:Dog>(_:WhatType.Type) -> WhatType {
    let d = WhatType.init(name:"Fido")
    return d
}

let dog = dogMakerAndNamer(NoisyDog.self) // dog is typed as NoisyDog

func dogMakerAndNamer(_:NoisyDog.Type) -> NoisyDog {
    let d = NoisyDog.init(name:"Fido")
    return d
}

In that call, we pass NoisyDog.self as the parameter. That tells the compiler what WhatType is! It is NoisyDog. In effect, the compiler now substitutes NoisyDog for WhatType throughout the generic


Generic Declarations

protocol Flier {
    func flockTogetherWith(_ f:Self)
}

In a protocol body, use of the keyword Self turns the protocol into a generic. Self here is a placeholder meaning the type of the adopter.

protocol Flier {
    associatedtype T
    func flockTogetherWith(_ f:T)
    func mateWith(_ f:T)
}

struct Bee {}
struct Bird : Flier {
    func flockTogetherWith(_ f:Bee) {}
    func mateWith(_ f:Bee) {}
}

A protocol can declare an associated type using an associatedtype statement. This turns the protocol into a generic; the associated type name is a placeholder.

func takeAndReturnSameThing<T> (_ t:T) -> T {
    print(T.self)
    return t
}

A function declaration can use a generic placeholder type for any of its parameters, for its return type, and within its body.

func takeAndReturnSameThing<T> (_ t:T) -> T {
    print(T.self)
    return t
}

let thing = takeAndReturnSameThing("howdy")

Here, the type of the argument "howdy" used in the call resolves T to String; therefore this call to takeAndReturnSameThing must also return a String, and the type of the variable capturing the result, thing, is inferred as String.

func takeAndReturnSameThing<T> (_ t:T) -> T {
    if T.self is String.Type {
        // ...
    }
    return t
}

If we call takeAndReturnSameThing("howdy"), the condition will be true. That sort of thing, however, is unusual; a generic whose behavior depends on interrogation of the placeholder type may need to be rewritten in some other way.


protocol Flier {
    func fly()
}
protocol Flocker {
    associatedtype T : Flier // *
    func flockTogetherWith(f:T)
}
struct Bee : Flier {
    func fly() {}
}
struct Bird : Flocker {
    func flockTogetherWith(f:Bee) {}
}

In that example, Flocker’s associated type T is constrained to be an adopter of Flier. Bee is an adopter of Flier; therefore Bird can adopt Flocker by specifying that the parameter type in its flockTogetherWith implementation is Bee.

protocol Flocker {
    func flockTogetherWith(f:Flier)
}

struct Bird : Flocker {
    func flockTogetherWith(f:Flier) {}
}

That’s not the same thing! That requires that a Flocker adopter specify the parameter for flockTogetherWith as Flier.

For a generic function or a generic object type, the type constraint can appear in the angle brackets. For example, earlier I described a global function func dogMakerAndNamer<WhatType:Dog>; Dog is a class, so the constraint says that WhatType must be Dog or a Dog subclass.

func myMin<T>(_ things:T...) -> T {
    var minimum = things.first!
    for item in things.dropFirst() {
        if item < minimum { // compile error
            minimum = item
        }
    }
    return minimum
}

The problem is the comparison item < minimum. How does the compiler know that the type T, the type of item and minimum, will be resolved to a type that can in fact be compared using the less-than operator in this way? It doesn’t, and that’s exactly why it rejects that code.

func myMin<T:Comparable>(_ things:T...) -> T {

Now myMin compiles, because it cannot be called except by resolving T to an object type that adopts Comparable and hence can be compared with the less-than operator. Naturally, built-in object types that you think should be comparable, such as Int, Double, String, and Character, do in fact adopt the Comparable protocol!



Explicit Specialization

In the generic examples so far, the placeholder’s type has been resolved mostly through inference. But there’s another way to perform resolution: we can resolve the type manually. This is called explicit specialization.

protocol Flier {
    associatedtype T
}
struct Bird : Flier {
    typealias T = String
}

The adopter of a protocol can resolve an associated type manually through a type alias equating the associated type with some explicit type

class Dog<T> {
    var name : T?
}
let d = Dog<String>()

The user of a generic object type can resolve a placeholder type manually using the same angle bracket syntax used to declare the generic in the first place

func dogMakerAndNamer<WhatType:Dog>(_:WhatType.Type) -> WhatType {
    let d = WhatType.init(name:"Fido")
    return d
}

You cannot explicitly specialize a generic function. One solution is to make your generic function take a type parameter resolving the generic.

protocol Flier {
    init()
}
struct Bird : Flier {
    init() {}
}
struct FlierMaker<T:Flier> {
    static func makeFlier() -> T {
        return T()
    }
}
let f = FlierMaker<Bird>.makeFlier() // returns a Bird


Chapter 6. Structured Concurrency

Multithreading



Multithreading



Structured Concurrency Syntax



Tasks



Wrapping a Completion Handler

profile
Hi 👋🏻 I'm an iOS Developer who loves to read🤓

0개의 댓글