[Swift] Struct, Class, Actor
8 min readSep 3, 2023
Struct, Class, Actor
Value VS Reference
Value Type:
- Struct, Enum, String, Int, etc.
- Stored in Stack
- Faster than reference types
- Thread safe
- When you assign or pass value type a new copy of data is created.
Reference type:
- Class, Function, Actors
- Stored in Heap
- Slower than value types. However, synchronization is guaranteed (if the value of the reference type is changed at a specific reference pointer, synchronization is guaranteed at other reference pointers as well)
- Not Thread safe (Except Actor)
- When you assign or pass reference type a new reference to the original instance will be created (pointer).
Stack:
- Stores Value Types
- variables allocated on the stack are stored directly in the memory, and access to this memory is very fast
- Each thread has its own stack
Heap:
- Stores Reference types
- Shared across threads
Struct, Class, Actor related code
Struct
import Foundation
import SwiftUI
struct CustomStrut{
var title:String
// Default Initializer from Swift
}
struct CustomStruct2{
var title:String
func makeNewStruct(_ title:String) -> CustomStruct2{
return CustomStruct2(title: title)
}
mutating func updateTitle(_ newTitle:String){
self.title = newTitle
}
}
struct CustomStruct3{
private(set) var title:String
// private(set) means set only property within the struct
init(title: String) {
self.title = title
}
func getTitle() -> String{
return title
}
mutating func setTile(_ title: String){
self.title = title
}
}
extension StructClassActor{
func customStructTest1(){
print("customStructTest1")
let objectA = CustomStrut(title: "Starting Tile!")
print("objectA: \(objectA.title)")
print("Pass the value of objectA to objectB")
var objectB = objectA
print("objectB: \(objectB.title)")
// ObjectA : Starting Title
// ObjectB : Starting Title
objectB.title = "Second Title"
// let title -> var title, let objectB -> var objectB. complier changes it
print("objectB: Title Changed!")
print("objectA: \(objectA.title)")
print("objectB: \(objectB.title)")
// objectA: Starting Tile!
// ObjectB : Second Title -> ObjectA <-> ObjectB not referencing themselves. (: Value Type)
/*
customStructTest1
objectA: Starting Tile!
Pass the value of objectA to objectB
objectB: Starting Tile!
objectB: Title Changed!
objectA: Starting Tile!
objectB: Second Title
*/
}
func customStruct2Test(){
print("customStruct2Test")
var struct1 = CustomStrut(title: "Title1")
print("Struct1: \(struct1.title)")
struct1.title = "Title2"
print("Struct1: \(struct1.title)")
// Struct1: Title1
// Struct1: Title2
var struct2 = CustomStruct2(title: "Title1")
print("Struct2: \(struct2.title)")
struct2 = CustomStruct2(title: "Title2")
// totally new struct assigned
print("Struct2: \(struct2.title)")
// Struct2: Title1
// Struct2: Title2
var struct3 = CustomStruct2(title: "Title1")
print("Struct3: \(struct3.title)")
struct3 = struct3.makeNewStruct("Title2")
print("struct3: \(struct3.title)")
// Totally new Structure
//Struct3: Title1
//struct3: Title2
var struct4 = CustomStruct2(title: "Title1")
print("Struct4: \(struct4.title)")
struct4.updateTitle("Title2")
print("Struct4: \(struct4.title)")
//Struct4: Title1
//Struct4: Title2
/*
customStruct2Test
Struct1: Title1
Struct1: Title2
Struct2: Title1
Struct2: Title2
Struct3: Title1
struct3: Title2
Struct4: Title1
Struct4: Title2
*/
}
func customStruct3Test(){
var struct1 = CustomStruct3(title: "Title1")
print("Struct1: \(struct1.getTitle())")
struct1.setTile("Title2")
print("struct1 \(struct1.getTitle())")
// Struct1: Title1
// Struct1 Title2
// get, set method to handle data inside struct
}
}
- Structure that copies the value itself → Creates a new structure when passing or assigning a value
- When changing the value of a variable in a structure → you must specify that you are changing it
mutating
through a keyword.self
- How to return a structure containing new stored properties: (1).
mutating
How to change the value of a stored property declared as a variable using a keyword (2). How to assign a new structure and overwrite a specific value declared as a variable
mutatinginout
It seems to be in the same context as changing this to !The reason why you can change the value of a stored property declared as an instance variable after receiving a specific text instance
let
as a constant: Alet
constant means that the pointer pointing to the class does not change, and the internal value is (if declared as a variable) ) Because it can be accessed and changed through the address value.
Class
import Foundation
// Class
class CustomClass{
var title: String
init(title: String) {
self.title = title
}
func updateTitle(_ title:String){
self.title = title
}
// Does not have to set "mutating" keyward inside Class
}
extension StructClassActor{
func customClassTest1(){
print("customClassTest1")
let objectA = CustomClass(title: "Starting Tile!")
print("objectA: \(objectA.title)")
print("Pass the REFERENCE of objectA to objectB")
let objectB = objectA
print("objectB: \(objectB.title)")
// objectA: Starting Tile!
// objectB: Starting Tile!
objectB.title = "Second Title"
// let title -> var title. complier changes it: not 'let' objectB. (let objectB remains same)
print("objectB: Title Changed!")
print("objectA: \(objectA.title)")
print("objectB: \(objectB.title)")
// objectA: Second Title!
// ObjectB : Second Title -> ObjectA === ObjectB referencing same place (: Reference Type)
print(objectA === objectB)
// true (=== operator to check the reference)
/*
customClassTest1
objectA: Starting Tile!
Pass the REFERENCE of objectA to objectB
objectB: Starting Tile!
objectB: Title Changed!
objectA: Second Title
objectB: Second Title
true
*/
}
func customClassTest2(){
print("customClassTest2")
let class1 = CustomClass(title: "Title1")
print("Class1: \(class1.title)")
class1.title = "Title2"
print("Class2: \(class1.title)")
//Class1: Title1
//Class2: Title2
let class2 = CustomClass(title: "Title1")
print("Class2: \(class2.title)")
class2.updateTitle("Title2")
print("Class2: \(class2.title)")
}
}
- Class that copies address values → Creates a new ‘pointer’ when assigned to a new variable
- When changing the value of a variable within a class →
mutating
No keyword. This means that it can be changed as is.
Actor
import Foundation
import SwiftUI
actor CustomActor{
var title:String
init(title: String) {
self.title = title
}
func updateTitle(_ title:String){
self.title = title
}
}
extension StructClassActor{
func customActorTests(){
Task{
// Need to get async -> Call them inside 'Task' block
print("customActorTests")
let actor1 = CustomActor(title: "Title1")
await print("Actor1: \(actor1.title)")
// Actor1: Title1
// actor1.title = "Title2" -> Ban
// Actor-isolated property 'title' can not be mutated from a non-isolated context
await actor1.updateTitle("Title2")
await print("Actor2: \(actor1.title)")
// Actor1 title : Actor2
/*
customActorTests
Actor1: Title1
Actor2: Title2
*/
}
}
}
actor
Because the internal propertiesactor-isolated
are properties, they cannot be changed in general ways → Accessactor
through functions declared inside the classself
, and specific property values can be changed →await
Through this, you can check whether the asynchronous operation has finished andTask
execute it internally.
Struct VS Class VS Actor
Struct
- Based on VALUES
- Can be mutated
- Stored in the Stack
Class
- Based on REFERENCE (INSTANCES)
- Stored in Heap
- Inherit from other classes
Actor
- Based on REFERENCE (INSTANCES)
- Stored in Heap
- Thread safe
Unlike classes, structures cannot be inherited. However, through the use of protocol + extension
When to use structs, classes, actors
STRUCTS: Data Model, Views
CLASS: ViewModels
ACTOR: Shared “Managers” and “Data store”
ARC
- Automatic Reference Counting: The number of times a class type is used is automatically counted and becomes the standard for determining memory allocation.
- When ARC becomes 0, the class is automatically deleted from memory (
deinit
can be checked directly through ), enabling efficient memory use.
ARC related code
import Foundation
import SwiftUI
//MARK: - ARC
class StrongClass1{
var title:String
var strongClass2: StrongClass2?
var weakClass: WeakClass?
init(title: String, strongClass2: StrongClass2? = nil, weakClass: WeakClass? = nil) {
self.title = title
self.strongClass2 = strongClass2
self.weakClass = weakClass
print("\(title) is initialized")
}
deinit{
print("\(title) is deinitialized")
}
}
class StrongClass2{
var title:String
var strongClass1: StrongClass1?
init(title: String, strongClass1: StrongClass1? = nil) {
self.title = title
self.strongClass1 = strongClass1
print("\(title) is initialized")
}
deinit{
print("\(title) is deinitialized")
}
}
class WeakClass{
var title:String
weak var strongClass1: StrongClass1?
init(title: String, strongClass1: StrongClass1? = nil) {
self.title = title
self.strongClass1 = strongClass1
print("\(title) in initialized")
}
deinit{
print("\(title) is deinitialized")
}
}
extension StructClassActor{
func classTest4(){
var class1: StrongClass1? = StrongClass1(title: "Strong Class1")
var class2: StrongClass2? = StrongClass2(title: "Strong Class2")
//Strong Class1 is initialized
//Strong Class2 is initialized
class1?.strongClass2 = class2
class2?.strongClass1 = class1
class1?.strongClass2 = nil
class2?.strongClass1 = nil
class1 = nil
class2 = nil
//Strong Class1 is deinitialized
//Strong Class2 is deinitialized
class1 = StrongClass1(title: "Strong Class1")
class2 = StrongClass2(title: "Strong Class2")
//Strong Class1 is initialized
//Strong Class2 is initialized
class1?.strongClass2 = class2
class2?.strongClass1 = class1
class1 = nil
class2 = nil
//Strong Class1 is initialized
//Strong Class2 is initialized
// Still Strong reference cycle retained -> Memory leaking
}
func classTest5(){
var class1:StrongClass1? = StrongClass1(title: "Strong Class1")
var class2:WeakClass? = WeakClass(title: "Weak Class")
//Strong Class1 is initialized
//Weak Class in initialized
class1?.weakClass = class2
class2?.strongClass1 = class1
class1 = nil
class2 = nil
// Strong Class1 is deinitiated
// Weak Class is deinitiated
// strongClass1 inside class2(Weak Class instance) -> nil, then ARC -> 0, then class1 -> deinit
// weak reference -> make its non-having strong reference less than strong reference
}
}
- Situations where a strong reference cycle may occur → A weak reference
nil
lets you know in advance that this will eventually become a reference, so you can lessen the worry about memory leaks. - Even with strong references
nil
, ARC can be adjusted by reducing counting through appropriate allocation before use. It is also important to note that the chain can be broken by using strong and weak references together.
MVVM
- View, view model, and model design patterns → Design patterns suitable for views that re-render into structures, view models, and models referenced through classes.
- View: Continuously re-creating a new view struct according to the initializer → COW (Copy On Write), a method that is much more suitable than a class view implementation due to the very high speed of the struct itself.
- View Model: The value
@StateObject
observed in the raw viewObservableObject
continues to maintain its value even during view re-rendering. If@ObjservedObject
you are observing withDeinit
- Model: If you are using the service class from multiple views and multiple points of view,
actor
You can ensure synchronization throughout the class.
MVVM related code
import Foundation
import SwiftUI
actor StructClassActorDataManager{
init() {
print("StructClassActorDataManager Init")
}
deinit {
print("StructClassActorDataManager Deinit")
}
func getDataFromDatabase() {
}
}
class StructClassActorViewModel: ObservableObject{
@Published var title:String = "MVVM"
let manager: StructClassActorDataManager
init(manager: StructClassActorDataManager) {
self.manager = manager
print("StructClassActorViewModel Init")
}
deinit {
print("StructClassActorViewModel Deinit")
}
}
struct StructClassActorView: View{
@StateObject private var viewModel = StructClassActorViewModel(manager: StructClassActorDataManager())
// -> ObservableObject class: Init ...
// Even if this entire struct View would be re-rendered, its StateObject would be same
// @ObservedObject private var viewModel = StructClassActorBootCampViewModel()
// -> ObservableObject class: Init and Deinit ...
let isActive:Bool
init(isActive:Bool){
self.isActive = isActive
print("View Init, isActive : \(isActive)")
/*
View Init, isActive : true
View Init, isActive : false
View Init, isActive : true
View Init, isActive : false
... -> Whenever isActive toggled, this View has been initiated.
*/
}
var body: some View{
ZStack{
Color.black.ignoresSafeArea()
Text(viewModel.title)
.font(.headline)
.foregroundColor(.white)
.fontWeight(.bold)
.frame(maxWidth: .infinity,maxHeight: .infinity)
.background(isActive ? .pink : .purple)
}
}
}
struct StructClassActorHomeView: View{
@State private var isActive:Bool = false
var body: some View{
StructClassActorView(isActive: isActive)
.onTapGesture {
isActive.toggle()
}
//MARK: - @StateObject
/*
View Init, isActive : false
StructClassActorDataManager Init
StructClassActorViewModel Init
View Init, isActive : true
View Init, isActive : false
View Init, isActive : true
View Init, isActive : false
View Init, isActive : true
View Init, isActive : false
*/
//MARK: - @ObservedObject
/*
StructClassActorDataManager Init
StructClassActorViewModel Init
View Init, isActive : false
StructClassActorDataManager Init
StructClassActorViewModel Init
View Init, isActive : true
StructClassActorViewModel Deinit
StructClassActorDataManager Deinit
StructClassActorDataManager Init
StructClassActorViewModel Init
View Init, isActive : false
StructClassActorViewModel Deinit
StructClassActorDataManager Deinit
StructClassActorDataManager Init
StructClassActorViewModel Init
View Init, isActive : true
StructClassActorViewModel Deinit
StructClassActorDataManager Deinit
*/
}
}
HomeView
The view, which is a child view, is continuously regenerated asStructClassActor
the parameter values required for the initializer change → view re-renderingisActive
StructClassActorViewModel
ObservableObject
Because the view is subscribed@StateObject
to is observed, even if view rendering occurs, it does not occurInit
After the initial stage → the existing reference value can be maintained as is.Deinit