- One of the things that distinguishes
SwiftUI
from its predecessors likeUIKit
andAppKit
is that theview
are primarily declared asvalue type
likestruct
instead ofclass
. - This is one of the changes in the architectural design that makes the
SwiftUI
API
light and flexible. This change sometimes makesdeveloper
including me often confused because of the knowledge of object oriented programming has been used before. - So in this article, let’s take the time to carefully study the meaning and usage of
SwiftUI
in declaring, interacting with the UI and moreover finding ways to have can find new methods better than usingUIKit
,AppKit
in new projects.
1 / The role of the property body:
Property body
inView Protocol
is probably the most confusing thing inSwiftUI
especially when it is closely related toview
update
as well asrendering cycle
.- In
UIKit
,AppKit
we usemethod
likeviewDidLoad
orlayoutSubviews
to recognize system events as well as process logic while withSwiftUI
body property
we can render the view without using the abovemethod
. body property
allows us torender view
based on its currentstate
and the system will rely on its currentstate
to see if it is necessary to re-render
the view. For example, when building aUIKit
ViewController
we oftentrigger
model
update
with theviewWillAppear
method
to ensure that theviewController
alwaysrender
theview
with the latestdata
:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">ArticleViewController</span> <span class="token punctuation">:</span> <span class="token builtin">UIViewController</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">let</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function">viewWillAppear</span> <span class="token punctuation">(</span> <span class="token number">_</span> animated <span class="token punctuation">:</span> <span class="token builtin">Bool</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">super</span> <span class="token punctuation">.</span> <span class="token function">viewWillAppear</span> <span class="token punctuation">(</span> animated <span class="token punctuation">)</span> viewModel <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- When we switch to
S
wiftUIthay vì ý tưởng
renderinglại
viewlại
mỗi khi
viewWillAppearthì chúng ta triển khai việc
viewModel thatsẽ
updatekhi xử lý
latest body property`:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">struct</span> <span class="token builtin">ArticleView</span> <span class="token punctuation">:</span> <span class="token builtin">View</span> <span class="token punctuation">{</span> @ <span class="token builtin">ObservedObject</span> <span class="token keyword">var</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token keyword">var</span> body <span class="token punctuation">:</span> some <span class="token builtin">View</span> <span class="token punctuation">{</span> viewModel <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token builtin">VStack</span> <span class="token punctuation">{</span> <span class="token function">Text</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">.</span> article <span class="token punctuation">.</span> text <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- However, the problem with the above implementation is that the
view
body
will be compared as soon as theviewModel
changes which means we will cause a lot of unnecessarymodel
update
. - It is easy to see that the
body property
not a convenient place to handle these unnecessaryupdate
but insteadSwiftUI
provides some similar features as inUIKit
,AppKit
. We are talking about theonAppear
customization similar to theviewWillAppear
incontrolelr
:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">struct</span> <span class="token builtin">ArticleView</span> <span class="token punctuation">:</span> <span class="token builtin">View</span> <span class="token punctuation">{</span> @ <span class="token builtin">ObservedObject</span> <span class="token keyword">var</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token keyword">var</span> body <span class="token punctuation">:</span> some <span class="token builtin">View</span> <span class="token punctuation">{</span> <span class="token builtin">VStack</span> <span class="token punctuation">{</span> <span class="token function">Text</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">.</span> article <span class="token punctuation">.</span> text <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span> <span class="token function">onAppear</span> <span class="token punctuation">(</span> perform <span class="token punctuation">:</span> viewModel <span class="token punctuation">.</span> update <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
2 / The initializer problem:
Life cycle
is an important issue that everydeveloper
should keep in mind when working withview
. In fact, it is easy to see that theview
inSwiftUI
do not have a proper life cycle because we use thevalue type
and not thereference type
.- When we want to change a
ArticleView
and viewModel update whenever theapp
isresume
when moved tobackground
instead of every timeviewappear
. One way we do that is to track each object through theNotificationCenter
on initialization as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <span class="token keyword">struct</span> <span class="token builtin">ArticleView</span> <span class="token punctuation">:</span> <span class="token builtin">View</span> <span class="token punctuation">{</span> @ <span class="token builtin">ObservedObject</span> <span class="token keyword">var</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token keyword">private</span> <span class="token keyword">var</span> cancellable <span class="token punctuation">:</span> <span class="token builtin">AnyCancellable</span> <span class="token operator">?</span> <span class="token keyword">init</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> viewModel <span class="token operator">=</span> viewModel cancellable <span class="token operator">=</span> <span class="token builtin">NotificationCenter</span> <span class="token punctuation">.</span> <span class="token keyword">default</span> <span class="token punctuation">.</span> <span class="token function">publisher</span> <span class="token punctuation">(</span> <span class="token keyword">for</span> <span class="token punctuation">:</span> <span class="token builtin">UIApplication</span> <span class="token punctuation">.</span> willEnterForegroundNotification <span class="token punctuation">)</span> <span class="token punctuation">.</span> sink <span class="token punctuation">{</span> <span class="token number">_</span> <span class="token keyword">in</span> viewModel <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">var</span> body <span class="token punctuation">:</span> some <span class="token builtin">View</span> <span class="token punctuation">{</span> <span class="token builtin">VStack</span> <span class="token punctuation">{</span> <span class="token function">Text</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">.</span> article <span class="token punctuation">.</span> text <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- The above implementation works normally until we get the
ArticleView
into otherview
. To express this case we create manyArticleView
value
likeArticleListView
by usingList
andNavigationLink
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">struct</span> <span class="token builtin">ArticleListView</span> <span class="token punctuation">:</span> <span class="token builtin">View</span> <span class="token punctuation">{</span> @ <span class="token builtin">ObservedObject</span> <span class="token keyword">var</span> store <span class="token punctuation">:</span> <span class="token builtin">ArticleStore</span> <span class="token keyword">var</span> body <span class="token punctuation">:</span> some <span class="token builtin">View</span> <span class="token punctuation">{</span> <span class="token function">List</span> <span class="token punctuation">(</span> store <span class="token punctuation">.</span> articles <span class="token punctuation">)</span> <span class="token punctuation">{</span> article <span class="token keyword">in</span> <span class="token function">NavigationLink</span> <span class="token punctuation">(</span> article <span class="token punctuation">.</span> title <span class="token punctuation">,</span> destination <span class="token punctuation">:</span> <span class="token function">ArticleView</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">:</span> <span class="token function">ArticleViewModel</span> <span class="token punctuation">(</span> article <span class="token punctuation">:</span> article <span class="token punctuation">,</span> store <span class="token punctuation">:</span> store <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
NavigationLink
will ask us to providedestination
details for each incomingview
and we havesetup
inNotificationCenter
when creatingArticleView
. Theobservation
will be immediatelyactive
even though theview
are not yetrender
.- Therefore we should implement the
function
as small as possible and theArticleView
will be updated when theapp
is moved back to theforeground
instead ofupdate
eachArticleViewModel
one by one. - To implement the above implementation we need
onReceive
instead of usingNotificationCenter
to track theview
initialization. Plus we no longer needCombine cancellable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token keyword">struct</span> <span class="token builtin">ArticleView</span> <span class="token punctuation">:</span> <span class="token builtin">View</span> <span class="token punctuation">{</span> @ <span class="token builtin">ObservedObject</span> <span class="token keyword">var</span> viewModel <span class="token punctuation">:</span> <span class="token builtin">ArticleViewModel</span> <span class="token keyword">var</span> body <span class="token punctuation">:</span> some <span class="token builtin">View</span> <span class="token punctuation">{</span> <span class="token builtin">VStack</span> <span class="token punctuation">{</span> <span class="token function">Text</span> <span class="token punctuation">(</span> viewModel <span class="token punctuation">.</span> article <span class="token punctuation">.</span> text <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span> <span class="token function">onReceive</span> <span class="token punctuation">(</span> <span class="token builtin">NotificationCenter</span> <span class="token punctuation">.</span> <span class="token keyword">default</span> <span class="token punctuation">.</span> <span class="token function">publisher</span> <span class="token punctuation">(</span> <span class="token keyword">for</span> <span class="token punctuation">:</span> <span class="token builtin">UIApplication</span> <span class="token punctuation">.</span> willEnterForegroundNotification <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token number">_</span> <span class="token keyword">in</span> viewModel <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- When
SwiftUI
view is initialized, it does not mean it will be displayed or used. That’s whySwiftUI
requires us to first createview
instead of instantiating them one by one.
3 / Ensuring that UIKit and AppKit views can be properly reused:
- We can put
UIKit
,AppKit
to use with SwiftUI throughProtocol
UIViewPresentable
and we will be responsible for creating andupdate
instance
ofview
being displayed and used. - To illustrate, let’s
render
NSAttributedString
using aUIKit
instance
likeUILabel
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">struct</span> <span class="token builtin">AttributedText</span> <span class="token punctuation">:</span> <span class="token builtin">UIViewRepresentable</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> string <span class="token punctuation">:</span> <span class="token builtin">NSAttributedString</span> <span class="token keyword">private</span> <span class="token keyword">let</span> label <span class="token operator">=</span> <span class="token function">UILabel</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">func</span> <span class="token function">makeUIView</span> <span class="token punctuation">(</span> context <span class="token punctuation">:</span> <span class="token builtin">Context</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">UILabel</span> <span class="token punctuation">{</span> label <span class="token punctuation">.</span> attributedText <span class="token operator">=</span> string <span class="token keyword">return</span> label <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">updateUIView</span> <span class="token punctuation">(</span> <span class="token number">_</span> view <span class="token punctuation">:</span> <span class="token builtin">UILabel</span> <span class="token punctuation">,</span> context <span class="token punctuation">:</span> <span class="token builtin">Context</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// No-op</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- However, to use the above implementation we need to solve two major problems
- In case we initialize
UILabel
byassign
it to the property means that we can not re-initialize klaij it wheneverstruct
is initialized again. - Do not
update
the view withupdateUIView
method
,label
will continuerender
attributedText
like old andassign
formakeUIView
although thestring
has beenupdate
.
- In case we initialize
makeUIView
not createUILabel
with themakeUIView
method. We alwaysassign
thelabel
string
with theattributedText
updateUIView
property
every time theupdateUIView
is called:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">struct</span> <span class="token builtin">AttributedText</span> <span class="token punctuation">:</span> <span class="token builtin">UIViewRepresentable</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> string <span class="token punctuation">:</span> <span class="token builtin">NSAttributedString</span> <span class="token keyword">func</span> <span class="token function">makeUIView</span> <span class="token punctuation">(</span> context <span class="token punctuation">:</span> <span class="token builtin">Context</span> <span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token builtin">UILabel</span> <span class="token punctuation">{</span> <span class="token function">UILabel</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">updateUIView</span> <span class="token punctuation">(</span> <span class="token number">_</span> view <span class="token punctuation">:</span> <span class="token builtin">UILabel</span> <span class="token punctuation">,</span> context <span class="token punctuation">:</span> <span class="token builtin">Context</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> view <span class="token punctuation">.</span> attributedText <span class="token operator">=</span> string <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
- With the above method, finally our
UILabel
can be reused andattributedText
is alwaysupdate
with thewrapper
string
property
.