-
Chapter 3: Diving Deeper Into SwiftUIRaywenderlich/SwiftUI by Tutorials 2021. 3. 19. 14:23
Version
Swift 5.3, iOS 14, Xcode 12
SwiftUI의 선언적(declarative) 스타일(style)을 사용하면, 눈길을 끄는(eye-catching) 디자인(designs)을 쉽게 구현(implement)할 수 있다. 이 장(chapter)에서는 SwiftUI 수정자(modifiers)를 사용하여, RGBullsEye를 최신 디자인 트렌드(the latest design trend)인 뉴모피즘(neumorphism) 디자인으로 변경(makeover)한다.
Views and modifiers
ContentView.swift에서 캔버스(canvas)를 연 상태에서 + 버튼(button)을 클릭하거나 Command-Shift-L을 눌러 라이브러리(Library)를 연다:
Library of primitive views and modifiers SwiftUI 뷰(view)는 UI의 일부(piece)이다. 작은 뷰(views)를 결합(combine)하여 더 큰 뷰(views)를 만든다. 사용자 정의 뷰(custom views)의 기본 구성 요소(basic building blocks)로 사용할 수있는
Text
와Color
같은 기본 뷰(primitive views)가 많이 있다.첫 번째 탭(tab)에는 컨트롤(control), 레이아웃(layout), 페인트(paints), 기타 뷰(Other Views)에 대한 기본 뷰(primitive views)가 나열되어 있다. 이들 중 대부분, 특히 컨트롤(controls)은 UIKit 요소(elements)로 친숙하지만, 일부 SwiftUI만의 고유한(unique) 요소도 있다. 다음 장(chapters)에서 사용 방법을 학습하게 될 것이다.
두 번째 탭에는 컨트롤(controls), 효과(effects), 레이아웃(layout), 텍스트(text) 등에 대한 수정자(modifiers)가 나열되어 있다. 수정자(modifier)는 기존(existing) 뷰(view)에서 새로운 뷰(view)를 생성하는 메서드(method)이다. 파이프 라인(pipeline)처럼 수정자(modifiers)를 연결(chain)하여 뷰(view)를 원하는 대로 수정할(customize) 수 있다.
SwiftUI는 재사용 가능한(reusable) 작은 뷰(views)를 생성한 다음, 해당 뷰를 사용하는 특정(specific) 컨텍스트(context)에 맞춰 수정자(modifiers)를 사용해 변경(customize)하도록 권장한다. 걱정할 것 없다. SwiftUI는 수정된(modified) 뷰(view)를 효율적인(efficient) 데이터 구조(data structure)로 축소(collapses)하므로 눈에 띄는 성능 저하 없이 이 모든 편의성(convenience)를 얻을 수 있다.
이러한 수정자(modifiers)를 모든 유형(type)의 뷰(view)에 적용(apply)할 수 있다. 곧 알게 되겠지만, 때로는 순서(order)가 중요하다.
Neumorphism
뉴모피즘(neumorphism)은 매우 평평한(super-flat) 최소한(minimal)의 UI에 반하는(pushback against) 새로운 스큐어모피즘(skeuomorphism, 있는 그대로의 모습을 사실적으로 표현하는 디자인 기법)이다. 뉴모픽(neumorphic) UI 요소(element)는 배경 아래에서 위로 밀어올라와(push up) 평면(flat) 3D 효과(effect)를 생성(producing)하는 것처럼 보인다.
Northwest and southeast highlights and shadows 요소(element)가 화면(screen)에서 약간 돌출(protrudes)되어 있고, 태양이 요소(element)의 북서쪽(northwest)으로 지고 있다(setting)고 상상해 본다. 이렇게 하면 왼쪽 상단 가장자리(upper-left edge)가 강조(highlight)되고 오른쪽 하단 가장자리(lower-right edge)에 그림자(shadow)가 생긴다. 또는 태양이 요소(element)의 남동쪽(southeast)에서 떠오른(rises)다면 오른쪽 하단 가장자리(lower-right edge)가 강조되고 그림자(shadow)는 왼쪽 상단 가장자리(upper left edge)에 생긴다.
이러한 강조(highlights)와 그림자(shadows)를 만들려면 세 가지 색상(colors)이 필요하다:
- 배경(background)과 요소(element) 표면(surface)의 중간색(neutral color)
- 강조(highlight)를 위한 더 밝은 색상(lighter color)
- 그림자(shadow)의 어두운 색상(darker color)
이 예제(example)에서는 눈에 잘 띄게(visible)하기 위해 고 대비(high contrast) 색상(colors)을 사용한다. 실제 프로젝트(project)에서는 보다 미묘한 효과(subtle effect)를 내는 색상(colors)을 사용한다.
RGBullsEye의 색상 원(), 레이블(labels), 버튼(button)에 강조(highlights)와 그림자(shadows)를 추가하여 이 Figma 디자인을 구현한다:
Figma design 이 디자인(design)은 375x812 포인트(point) 화면(iPhone 12 Pro)으로 설계(laid out)되었다. Figma 디자인(design)의 크기(size) 값(values)으로 디자인(design)을 설정(set up)한 다음, 이를 화면 크기에 따른(screen-size-dependent) 값(values)으로 변경(change)한다.
Note: 대다수의 개발자는 Figma/Sketch 디자인(design) 단계(step)를 건너 뛰고, SwiftUI에서 직접(directly) 설계(design)하기만 하면 된다.
Colors for neumorphism
시작 프로젝트(starter project)를 연다. 이전 장(chapter)의 최종 챌린지(challenge) 프로젝트(project)와 동일하지만,
ColorCircle
은size
매개 변수가 있는 자체 파일에 있으며 Assets.xcassets에는 라이트(light) 모드와 다크(dark) 모드 모두에 대한 Highlight, Shadow 색상(colors)이 포함되어 있다:- Element: #F2F3F7; Dark: #292A2E
- Highlight: #FFFFFF (20% opacity); Dark: #3E3F42
- Shadow: #BECDE2; Dark: #1A1A1A
ColorExtension.swift에는 다음과 같은 정적(static) 속성(properties)이 포함(includes)된다:
static let element = Color("Element") static let highlight = Color("Highlight") static let shadow = Color("Shadow")
Shadows for neumorphism
먼저 북서쪽(northwest)과 남동쪽(southeast) 그림자(shadows)에 대한 사용자 정의(custom) 수정자(modifiers)를 생성한다.
ViewExtension.swift라는 새로운 Swift 파일(new Swift file)을 만들고,
import Foundation
구문(statement)을 다음 코드로 바꾼다(replace):import SwiftUI extension View { func northWestShadow( radius: CGFloat = 16, offset: CGFloat = 6 ) -> some View { return self .shadow( color: .highlight, radius: radius, x: -offset, y: -offset) .shadow( color: .shadow, radius: radius, x: offset, y: offset) } func southEastShadow( radius: CGFloat = 16, offset: CGFloat = 6 ) -> some View { return self .shadow( color: .shadow, radius: radius, x: -offset, y: -offset) .shadow( color: .highlight, radius: radius, x: offset, y: offset) } }
shadow(color:radius:x:y:)
수정자(modifier)는 지정된(specified)color
와radius
크기(size)의 그림자(shadow)를 (x
,y
)만큼 오프셋(offset)하여 뷰(view)에 추가한다. 기본(default)Color
는 불투명도(opacity)가 0.33인 검은색이고 기본(default) 오프셋(offset)은 (0, 0)이다.북서쪽(northwest)과 남동쪽(southeast) 그림자(shadow) 수정자(modifiers)의 경우, 뷰(view)의 왼쪽 상단 모서리(upper-left corner, 음수 오프셋 값)에 그림자(shadow)를 적용하고 오른쪽 하단 모서리(lower-right corner, 양수 오프셋 값)에 다른 색상의 그림자(shadow)를 적용한다. 북서쪽(northwest) 그림자(shadow)의 경우 왼쪽 위 색상(upper-left color)은
highlight
이고 오른쪽 아래 색상(lower-right color)은shadow
이다. 남동쪽(southeast) 그림자(shadow)에서는 이 색상(colors)을 전환(switch)한다.색상 원(color circles)과 버튼(button)은 동일한 반지름(radius)과 오프셋(offset) 값(values)을 사용하므로, 이를 기본값(default values)으로 설정(set)한다. 그러나 나중에 텍스트(text) 레이블(labels)에서는 더 작은 값(values)이 필요하므로 인수(arguments)로 전달할 수 있다.
shadow
수정자(modifiers)를 적용(apply)하는 순서(order)는 중요하지 않다. 왼쪽 상단 모서리(upper-left corner)부터 적용했으므로 뉴모픽(neumorphic) 그림자(shadow)의 방향(direction)을 쉽게 시각화(visualize)할 수 있다.Setting the background color
이러한 그림자(shadows)가 작동하려면 뷰(view)의 배경(background)이 UI 요소(elements)와 동일한 색상(color)이어야 한다. 이를 설정하려면 ContentView.swift로 돌아간다.
ZStack
을 사용하여 전체 화면(entire screen)의 배경색(background color)을element
로 설정한다. Z방향(Z-direction)은 화면(screen) 표면(surface)에 수직(perpendicular)이므로 화면(screen) 위에 뷰(views)를 배치(layer)하는 좋은 방법이다.ZStack
클로저(closure)의 하위(lower) 항목일 수록 스택 뷰(stack view)에서 더 높게 나타난다. 첫 번째 뷰(view)를 화면(screen) 표면(surface)에 배치(placing)한 다음 그 위에 다음 뷰(view)를 배치(layering)한다고 생각하면 된다.다음은
VStack
을ZStack
에 포함(embed)시킨 다음,VStack
앞에Color.element
를 추가하는 작업이다:ZStack { Color.element VStack {...} }
미리보기(preview)를 새로 고친다(refresh).
VStack
아래(below)에Color
를 위치(layered)시켰지만 색상(color)이 안전 영역(safe area)으로 확장(extend)되지 않는다. 이를 수정(fix)하려면, 다음 수정자(modifier)를Color.element
에 추가한다:.edgesIgnoringSafeArea(.all)
Note: 이 수정자(modifier)를
Color
대신ZStack
에 추가할 수 있지만,ZStack
은 content views를 안전 영역(safe area)에 자유롭게 분산(spread)할 수 있다. 이 수정자는 원하는 내용이 아닐 수도 있다.이제 앱(app)은 배경(background)이 완전한 흰색(quite white)이 아니라는 점을 제외(except)하면 이전과 동일하다. 다음으로 색상 원(color circles)에 테두리(border)를 지정한 다음, 해당 테두리(border)에 강조(highlight)와 그림자(shadow)를 적용(apply)한다.
Creating a neumorphic border
테두리(border)를 생성하는 가장 쉬운 방법은
ZStack
을 사용하여 요소 색상 원(element
-colored circle) 위에 RGB 색상 원(RGB
-colored circle)을 배치(layer)하는 것이다.ColorCircle.swift에서
body
의 내용을 다음으로 바꾼다(replace):ZStack { Circle() .fill(Color.element) .northWestShadow() Circle() .fill(Color(red: rgb.red, green: rgb.green, blue: rgb.blue)) .padding(20) } .frame(width: size, height: size)
ZStack
에Circle()
을 포함(embed)하고 그 앞에 요소 색상 원(element
-coloredCircle
)을 추가 한 다음, 패딩(padding)을 추가하여RGB
원(circle)을 더 작게 만든다. 그림자(shadow) 효과(effect)를 얻으려면 테두리 원(border circle)에northWestShadow()
를 적용(apply)한다.Note: 수정자(modifier)
fill(_:style:)
은 shapes에만 적용할 수 있으므로, 수정자(modifiers)의 순서(order)를 변경(changing)하면 오류(error)가 발생한다.Circle() .padding(20) .fill(Color(red: rgb.red, green: rgb.green, blue: rgb.blue))
마지막으로
width
와height
를 모두size
로 설정(set)한다.필요한(necessary) 경우, 미리보기(preview)를 새로 고침(refresh)하여 어떻게 보이는지 확인한다:
Neumorphic color circle on white background 그림자(shadow)가 있지만,
ColorCircle
미리보기(preview)는 배경(background)이 흰색이어서 그림자(shadow)의 전체 효과(full effect)를 볼 수 없다.아래로 스크롤(scroll down)하여
previews
내용(contents)을 다음과 같이 변경(change)한다:ZStack { Color.element ColorCircle(rgb: RGB(), size: 200) } .frame(width: 300, height: 300) .previewLayout(.sizeThatFits)
ContentView
에서와 같은 방식으로element
에 배경색(background color)을 설정(set)한다. 미리보기(preview) 프레임(frame)이 이미 그림자(shadow)를 보여줄(show off)만큼 충분히 크게 설정되어 있으므로 걱정해야 할 안전 영역(safe area)가 없다.Neumorphic color circle on element-colored background 흰색이 아닌(not-quite-white) 배경(background)에 대해 왼쪽 상단 가장자리(upper left edge)의 강조(highlight)가 더 두드러지고(stands out), 오른쪽 하단 가장자리(lower-right edge)의 그림자(shadow)가 덜 어둡게 나타난다(appears).
previews
에서Color.element
를 주석 처리(comment out)하고 주석 해제(uncomment)하면서 직접 확인해 본다.이제 ContentView.swift로 돌아가서 미리보기(preview)를 새로 고친다(refresh):
Neumorphic target color circle 이제 원(circles)에 뉴모픽(neumorphic)이 적용 되었다.
Order of modifiers
뷰(view)에 둘 이상의 수정자(modifier)를 적용(apply)할 때, 때로는(sometimes) 순서(order)가 중요하다(matters).
padding
및frame
같은 수정자(modifiers)는 뷰(view)의 배치(layout) 또는 위치(position)를 변경(change)한다.background
또는border
와 같은 수정자(modifiers)는 뷰(view)를 채우거나(fill) 감싼다(wrap). 일반적으로(normally) 뷰(view)를 채우거나(fill) 감싸기(wrap) 전에 뷰(view)의 배치(layout)와 위치(position)를 설정(set up)하려고 한다.예를 들어(for example), ContentView.swift에서
Text(guess.intString)
의padding
수정자(modifier) 뒤에border
수정자(modifier)를 추가한다:.padding() .border(Color.purple)
Border around padded text 기본(default amount)
padding
은Text
뷰(view)의 텍스트(text)를 둘러싼(surrounds) 다음, 패딩된 텍스트(padded text) 주위(around)에 자주색(purple)border
를 표시(put)한다.이제 순서(order)를 변경(change)한다:
.border(Color.purple) .padding()
Padding around bordered text border
를 먼저 적용(apply)하면 텍스트(text)의 내부 영역(intrinsic area)을 둘러 싼다. 코드 편집기(code editor)에서padding()
을 선택(select)하여 위치를 확인하면 인접한 요소(neighboring elements)와 거리(distance)를 유지(keep)하고 있는 것을 알 수 있다..border(Color.purple)
를 삭제(delete)한다.일부 수정자(modifiers)는 특정 종류의 뷰(views)에만 적용(applied to)할 수 있다. 예를 들어(for example), 다음 수정자(modifiers)는
Text
뷰(views)에만 적용할 수 있다:Text modifiers 전부는 아니지만 이러한 수정자(modifiers) 중 일부는
Text
뷰(view)를 반환(return)한다. 예를 들어(for example),font
,fontWeight
,bold
,italic
은Text
뷰(view)를 수정(modify)하여 다른Text
뷰(view)를 생성(produce)한다. 따라서 이러한 수정자(modifiers)를 임의의 순서(order)로 적용(apply)할 수 있다.그러나
lineLimit
은some View
를 반환(returns)하므로, 오류(error)가 발생한다:Text(guess.intString) .lineLimit(0) .bold()
이 순서(order)는 괜찮다:
Text(guess.intString) .bold() .lineLimit(0)
"Intro to Controls: Text & Image"에서 수정자(modifiers) 사용에 대해 자세히 알아본다.
Creating a neumorphic button
다음으로 여전히 ContentView.swift에서 Hit Me! 버튼(button)을 튀어 오르게(pop) 만들어 본다.
그림자(shadow)를 드리우려면(cast), 버튼(button)이 보다 실질적인 모양(substantial shape)이어야 한다.
alert
전의 action 아래에 다음 수정자(modifiers)를 추가한다..frame(width: 327, height: 48) .background(Capsule())
배경(background)을 캡슐 모양(capsule shape)으로 설정(set)한다.
Capsule
은 모서리 반경 값(corner radius value)이 짧은 면(shorter side) 길이(length)의 절반(half)으로 설정(set to)된RoundedRectangle
이다. 이걸로 지정한(specified) 프레임(frame)을 채운다(fills).채우기 색상(fill color)은 기본적(defaults)으로
primary
이며, 라이트 모드(light mode)에서는 검은색이다.이것은 뉴모픽(neumorphic) 버튼(button)이므로,
Capsule()
에 다음 수정자(modifiers)를 추가(add)하여element
에 채우기 색상(fill color)을 설정(set)하고 북서쪽 그림자(northwest shadow)를 적용(apply)한다:.fill(Color.element) .northWestShadow()
그러면 뉴모픽(neumorphic) 버튼(button)은 다음과 같아진다:
Neumorphic button Creating a custom button style
사용자 정의(customizing) 버튼(button)을 만들 때, 사용자 정의 버튼 스타일(custom button style)을 만드는 것이 좋다. 이 앱(app)에서 재사용(reuse)할 계획(planning)이 없더라도, 코드가 덜 복잡(cluttered)해 진다. 특히(especially), 해당 버튼 스타일(button style)에 더 많은 옵션(options)을 추가하기로 결정(decide to)한 경우에는 더욱 그렇다.
따라서 NeuButtonStyle.swift라는 새로운 Swift 파일(new Swift file)을 생성하고,
import Foundation
을 다음 코드로 바꾼다(replace):import SwiftUI struct NeuButtonStyle: ButtonStyle { let width: CGFloat let height: CGFloat func makeBody(configuration: Self.Configuration) -> some View { configuration.label // Move frame and background modifiers here } }
ButtonStyle
은 버튼(button)의label
과 사용자(user)가 버튼(button)을 누를(pressing) 때true
가 되는 부울(Boolean), 두 가지 속성(properties)이 있는ButtonStyleConfiguration
을 제공(provides)하는 프로토콜(protocol)이다.makeBody(configuration:)
를 구현(implement)하여label
을 수정(modify)한다.Button
을 수정(modify)하는 방법을 이미 알고(figured out) 있으므로,ContentView
의Button
에서frame
과background
수정자(modifiers)를 잘라내어(cut)NeuButtonStyle
의configuration.label
아래에 붙여 넣는다(paste).그런 다음 프레임(frame)의
width
와height
값(values)을NeuButtonStyle
의 해당 속성(corresponding properties)으로 바꾼다(replace).이제 버튼(button) 스타일(style) 코드는 다음과 같다:
struct NeuButtonStyle: ButtonStyle { let width: CGFloat let height: CGFloat func makeBody(configuration: Self.Configuration) -> some View { configuration.label .frame(width: width, height: height) .background( Capsule() .fill(Color.element) .northWestShadow() ) } }
ContentView.swift로 돌아가,
Button
을 다음 코드로 수정(modify)한다:.buttonStyle(NeuButtonStyle(width: 327, height: 48))
이 너비(width) 값(value)은 12 Pro와 같은 iPhone에 적용된다. 더 작거나 큰 iPhone을 지원(support)하기 위해, 적합한(fit) 값(values)을 전달(pass)하는 방법에 대해 배울 것이다.
이제 미리보기(preview)를 새로 고친다(refresh). 이전과 동일해야 한다:
Neumorphic button using NeuButtonStyle 하지만 같지 않다. 버튼(button)의 레이블(label)은 이제 파란색이 아니라 검은색이다.
Fixing button style issues
사용자 정의(custom) 버튼 스타일(button style)을 만들면 사용자(user)가 버튼(button)을 탭(taps)할 때 기본 레이블 색상(default label color)과 기본 시각적 피드백(default visual feedback)이 사라진다.
이미 사용자 정의 색상(customcolor)을 사용하고 있다면, 레이블 색상(label color)은 문제가 되지 않는다. 그렇지 않은 경우
NeuButtonStyle
구조체(structure)의configuration.label
에 다음 수정자(modifier)를 추가하면 된다:.foregroundColor(Color(UIColor.systemBlue))
그러나 Figma 디자인(design)의 버튼(button) 텍스트(text)가 검은색이므로 문제가 되지 않는다.
이제 시각적 피드백(visual feedback) 문제(issue)를 해결해 본다.
버튼 스타일(button style)을 만들면, 실제로 사용자가 버튼(button)을 탭할(taps) 때 발생하는 작업을 정의(defining)할 책임(responsible for)이 있다. 실제로(in fact) 구성 레이블(configuration label)의 설명(description)은 "버튼을 누를 때의 효과를 설명하는 뷰(a view that describes the effect of pressing the button)"이다.
ContentView
를 실시간으로 미리보고(live-preview) 버튼(button)을 탭(tap)한다. 버튼(button)을 탭(tap)해도 버튼(button) 모양(appearance)이 전혀 변경되지 않는다. 이것은 좋은 사용자 경험(user experience)이 아니다. 다행히(fortunately), 기본 동작(default behavior)을 쉽게 복구 (recover)할 수 있다.ContentView.swift를 떠나기(leave) 전에, 캔버스(canvas)의 왼쪽 하단(lower-left corner)에 있는 핀 버튼(pin button)을 클릭(click)한다:
Pin the ContentView preview NeuButtonStyle.swift에서 작업하면서
ContentView
에 영향(affect)을 주는 변경(changes)을 수행한다. 미리보기(preview)를 고정(pinning)하면 두 파일 간에 앞뒤로(back and forth) 이동(bounce)할 필요없이 변경 사항(changes)의 효과(effect)를 확인할 수 있다.이제
NeuButtonStyle
의frame
수정자(modifier) 위에 다음 행(line)을 추가한다:.opacity(configuration.isPressed ? 0.2 : 1)
사용자(user)가 버튼(button)을 탭(taps)하면, 레이블(label)의 불투명도(opacity)를 줄여 표준 조광 효과(standard dimming effect)를 생성한다.
ContentView
의 실시간 미리보기(live preview)를 새로 고치고(refresh), 이것을 확인한다.뉴모픽(neumorphic) 버튼(button)을 활용(advantage)하고자 하는 경우, 사용자(user)가 버튼(button)을 탭(taps)할 때 그림자(shadow)의 방향(direction)을 끄거나(turn off) 전환(switch)할 수 있다.
NeuButtonStyle
에서background
를 다음으로 바꾼다(replace):Group { if configuration.isPressed { Capsule() .fill(Color.element) } else { Capsule() .fill(Color.element) .northWestShadow() } }
Group
은 또 다른 SwiftUI 컨테이너(container)로, 어떤 배치(layout)도 하지 않는다. 단일 뷰(single view)보다 복잡한(complicated) 코드를 래핑(wrap)해야 할 때 유용하다.사용자(user)가 버튼(button)을 누르면, 평평한 버튼(flat button)이 표시된다. 그렇지 않으면(otherwise), 그림자 버튼(shadowed button)이 표시된다.
실시간 미리보기(live preview)를 새로 고침(refresh)한 다음, 버튼(button)을 탭(tap)한다. 버튼을 누르고 있으면(hold down) 그림자(shadow)가 사라진다(disappears).
이에 대한 변형(variation)은
isPressed
가true
일 때southEastShadow()
를 적용(apply)하는 것이다:Group { if configuration.isPressed { Capsule() .fill(Color.element) .southEastShadow() // Add this line } else { Capsule() .fill(Color.element) .northWestShadow() } }
이제 실시간 미리보기(live preview)를 끈다(turn off).
Creating a beveled edge
다음으로 색상 원(color circles)의 레이블(labels)에 대한 새로운 모양(look)을 만든다. 다시
Capsule
을 사용하여 디자인(design)을 통합(unify)한다. 그러나 버튼(button)과 구별(differentiate)하기 위해 베벨 가장자리 효과(bevel edge effect)를 만든다.새 SwiftUI View 파일을 만들고(new SwiftUI View file) 이름을 BevelText.swift로 지정한다.
BevelText
구조체(structure)의 내용(contents)을 다음으로 바꾼다(replace):let text: String let width: CGFloat let height: CGFloat var body: some View { Text(text) }
ContentView.swift로 돌아가서
Text
뷰(views)와padding
을BevelText
뷰(views)로 바꾼다(replace):if !showScore { BevelText( text: "R: ??? G: ??? B: ???", width: 200, height: 48) } else { BevelText( text: game.target.intString, width: 200, height: 48) } ColorCircle(rgb: guess, size: 200) BevelText(text: guess.intString, width: 200, height: 48)
BevelText
뷰(views)는 프레임 높이(frame height)가 48 포인트(points)이므로padding
이 필요하지 않다.이제
BevelText
에 집중(focus on)할 수 있도록,ContentView
미리보기(preview)의 고정을 해제(unpin)한다.BevelText.swift로 돌아가서
previews
의 내용(contents)을 다음으로 바꾼다(replace):ZStack { Color.element BevelText( text: "R: ??? G: ??? B: ???", width: 200, height: 48) } .frame(width: 300, height: 100) .previewLayout(.sizeThatFits)
BevelText: Getting started element
의 색상 배경(color background) 위에BevelText
를 배치(layer)한다. 베벨 가장자리(bevel edge)가 있는 캡슐(capsule)을 만들기 위한 시작점(starting point)이다.BevelText
의body
에서Text
에 다음 두 수정자(modifiers)를 추가한다:.frame(width: width, height: height) .background( Capsule() .fill(Color.element) .northWestShadow(radius: 3, offset: 1) )
미리보기(preview)를 새로 고친다(refresh). 이는 외부(outer)의 캡슐 모양(capsule shape)으로
NeuButtonStyle
의 작은 버전(smaller version)이다:Outer Capsule with northwest shadow 이제 이것을
ZStack
에 포함(embed)하여, 다른Capsule
을 3 포인트(points)만큼 끼워 넣어(inset) 배치(layer)할 수 있다:ZStack { Capsule() .fill(Color.element) .northWestShadow(radius: 3, offset: 1) Capsule() .inset(by: 3) .fill(Color.element) .southEastShadow(radius: 1, offset: 1) }
태양이 북서쪽(northwest)으로 지는 것(setting)을 상상해 본다. 외부(outer) 왼쪽 상단 가장자리(upper-left edge)와 내부(inner) 오른쪽 하단 가장자리(lower-right edge)를 강조 표시하고, 내부(inner) 왼쪽 상단 가장자리(upper-left edge)와 외부(outer) 오른쪽 하단 가장자리(lower-right edge)에서 그림자(shadows)를 드리운다(casts).
이 효과(effect)를 얻으려면, 남동쪽(southeast) 그림자(shadow)를 내부(inner)
Capsule
에 적용한다.BevelText: Finished Note: Caroline Begbie의 이 우아하고(elegantly) 간단한(simple) 구현(implementation)에 대해 감사한다.
이제 ContentView.swift로 돌아가 결과를 확인한다:
Neumorphism accomplished! "Debugging" dark mode
Assets의 색상 세트(color sets)에는 다크 모드 값(dark mode values)이 있다. 이 다크 모드(dark mode)에서 이 디자인(design)이 어떻게 생겼는지 확인해 본다.
다크 모드(dark mode) 미리보기(preview)를 쉽게 확인할 수 있다. 실시간 미리보기(live-preview)가 켜져(on) 있으면 끄고, 미리보기(preview)의 속성(inspector)을 열어 Dark 색 구성(color scheme)을 선택(select)한다:
Set preview's color scheme to Dark. 색상 세트의(color sets) 덕분에 다크 모드(dark mode)의 그림자(shadows)를 바로 확인할 수 있다.
Neumorphism: Dark mode 그러나 문제(problem)가 있는 것 같다. 실시간 미리보기(live preview)를 다시 실행(fire up)하고 Hit Me!를 탭(tap)한다. 알림(alert)의 색 구성(color scheme)이 어둡지 않다.
Alert's color scheme isn't dark?! 이 문제를 해결(figure out)하기 위해 많은 시간을 보냈다. 그러나 이것은 미리보기(preview)에만 전적으로(entirely) 의존해서는 안되는 이유를 보여주는 좋은 예(example)이다.
iPhone 12 Pro 시뮬레이터(simulator)에서 앱(app)을 빌드(build)하고 실행(run)한다. 가장 먼저 눈에 띄는 것(notice)은 미리보기(preview)의 다크 모드 구성(dark color scheme)이 시뮬레이터(simulator)에 영향을 주지(affect) 않는다는 것이다.
문제(problem) 없다. 디버그 도구 모음(debug toolbar)에서 Environment Overrides 버튼(button)을 클릭하고 Appearance ▸ Dark를 선택한다:
Override color scheme while running in a simulator. Note: apple.co/2yJJk7T에서 내장된(built-in)
EnvironmentValues
목록(list)을 확인할 수 있다. 이들 중 대부분은 접근성(accessibility), 로케일(locale), 캘린더(calendar) 색 구성(color scheme)과 같은 장치(device) 사용자 설정(user settings)에 해당한다.이제 시뮬레이터(simulator)는 앱(app)을 다크 모드(dark mode)로 표시(displays)한다. Hit Me!를 탭(tap)한다:
Simulator: Alert's color scheme is dark. 다크 모드(dark mode)에서는 알림(alert)도 어두워야 한다.
따라서 미리보기(preview)에 예상한(expect) 내용이 표시되지 않는 경우, 보이지 않는 문제(phantom problem)을 해결(fix)하는 데 시간을 낭비하기 전에 시뮬레이터(simulated)나 실제 기기(real device)에서 실행(running)해 본다.
previews
에서 이 행(line)을 삭제(delete)하거나 주석 처리(comment)한다:.preferredColorScheme(.dark)
Modifying font
Figma 디자인(design)을 마무리(finishing touch)하려면 한 가지가 더 필요하다. 모든 텍스트(text)는 조금 더 크고(bigger) 굵게(bolder) 표시되어야 한다.
ContentView.swift에서 모든 UI 요소(elements)가 포함(contains)된
VStack
에 다음 수정자(modifier)를 추가한다:.font(.headline)
모든 하위 뷰(child views)에 영향을 주는(affects)
VStack
에 대해 뷰 수준(view-level)의 환경 값(environment value)을 설정(set)한다. 이제 모든 텍스트(text)는 headline 글꼴 크기(font size)를 사용한다:Headline font size applies to all the text. 이 전체(overall)
Text
수정자(modifier)를 재정의(override)할 수 있다. 예를 들어(for example),ColorSlider
구조체(structure)의HStack
에 다음 수정자(modifier)를 추가한다:.font(.subheadline)
이제 슬라이더 레이블(slider labels)은 더 작고(smaller) 얇은(not-bold) 글꼴(font)을 사용한다:
Slider labels use subheadline font size. Adapting to the device screen size
이제 이 디자인(design)이 더 작은 화면(smaller screen)에서 어떻게 보이는지 확인해 볼 시간이다. 더 작거나(smaller) 큰(larger) 화면(screen)에서 디자인(design)이 맞는지(fits) 확인(check)하려면 실행 대상 메뉴(run destination menu)에서 시뮬레이터(simulator)를 선택(select)하기만 하면 된다. 미리보기(preview)는 선택한 시뮬레이터(selected simulator)를 사용하도록 업데이트(update)된다.
실행 대상(run destination)을 iPhone 8로 변경(change)한다:
Run destination: iPhone 8 iPhone 8 화면(screen)의 높이(height)는 667 포인트(points)에 불과하므로, 버튼(button)이 보이지 않는다. 색상 원(color circles)을 더 작게 만들어 이 문제(problem)를 해결할 수 있습니다. 그러나 얼만큼 작게 만들어야 할지 생각해 봐야 한다.
이제 실행 대상(run destination)을 iPhone 12 Pro Max로 변경(change)한다:
Run destination: iPhone 12 Pro Max 이 화면(screen)의 높이(height)는 896 포인트(points)이므로 빈 공간(blank space)이 조금 더 있다. 다시 말하지만, 원(circles)을 더 크게 만들 수 있다. 그러나 얼만큼 크게 만들어야 할지 생각해 봐야 한다.
ContentView.swift에서
State
변수(variables) 아래에 다음 속성(properties)을 추가한다:let circleSize: CGFloat = 0.5 let labelWidth: CGFloat = 0.53 let labelHeight: CGFloat = 0.06 let buttonWidth: CGFloat = 0.87
원래(original) 375x812 Figma 디자인(design)에서 이 비율(proportions)을 계산했다(worked out). 이러한 분수(fractions)는 코드에 직접 연결(hard-wired)한 값(values)에 가깝다:
circleSize
* 375 = 187.5,labelWidth
* 375 = 199,labelHeight
* 812 = 49,buttonWidth
* 375 = 326.버튼(button) 높이(height)도
labelHeight
이다.Figma 디자인(design)은 원(circle)의 지름(diameter)이 레이블(label) 너비(width)와 같지만, 원(circles)은 작은 화면(screen)에 맞게(fit) 약간 더 축소(shrinking)해야 한다.
이제 코드가 화면 크기(screen size)의 높이(height)와 너비(width)를 감지(detect)할 수 있다면, 이러한 요소(elements)에 적합한 크기(right sizes)를 계산(calculate)할 수 있다.
Getting screen size from GeometryReader
GeometryReader
에ZStack
을 포함(embed)하여,size
와frame
값(values)에 접근(access)한다.Note: "17장(chapter) : Drawing & Custom Graphics"에서 GeometryReader에 대해 자세히 알아본다.
Command-click 메뉴(menu)에는 편리(handy)하게 사용할 수 있는 Embed...이 있다:
Embed ZStack in ... some container. ContentView.swift에서 이 제네릭(generic)
Container
에 최상위(top-level)ZStack
을 포함(embed)한 다음Container
를GeometryReader
로 변경(change)한다:GeometryReader { geometry in ZStack {
GeometryReader
는frame
메서드(method)와size
및safeAreaInset
속성(properties)이 있는GeometryProxy
객체(object)를 제공(provides)한다. 이 객체(object)의 이름을geometry
로 지정한다.두 개의
ColorCircle
생성자(initializers)에서size : 200
을 다음으로 교체(replace)한다:size: geometry.size.width * circleSize
세 개의
BevelText
생성자(initializers)에서width : 200, height : 48
를 다음으로 교체(replace)한다:width: geometry.size.width * labelWidth, height: geometry.size.height * labelHeight
(NeuButtonStyle(...))
을 다음으로 교체(replace)한다:(NeuButtonStyle( width: geometry.size.width * buttonWidth, height: geometry.size.height * labelHeight))
실행 대상(run destination)을 다시 iPhone 12 Pro로 변경(change)하고 미리보기(preview)를 새로 고쳐(refresh) 이전과 동일하게 보이는지 확인(check)한다.
Previewing different devices
세 가지 화면 크기(screen sizes)를 모두 한 번에(at once) 보려면, 두 개의 시뮬레이터(simulators)에서 앱(app)을 빌드(build)하고 실행(run)할 수 있다. 대신(instead),
ContentView_Previews
에 미리보기(previews)를 추가한다.Preview 도구 모음(toolbar)에서 Duplicate Preview 버튼(button)을 클릭(click)한다:
Duplicate-preview button 캔버스(canvas) 미리보기(preview)에 또 다른
ContentPreview
가 나타나고(appears), 코드 편집기(code editor)에Group
이 생성된다.Group { ContentView(guess: RGB()) ContentView(guess: RGB()) }
이 수정자(modifier)를
Group
의 첫 번째ContentView
에 추가한다:.previewDevice("iPhone 8")
기기(device)의 이름은 실행 대상 메뉴(run destination menu)에 있는 이름 중 하나와 일치(match)해야 한다. 예를 들어(for example), "iPhone SE"는 메뉴(menu)에선 "iPhone SE(2nd generation)"이므로 작동하지 않는다.
이제 캔버스(canvas)에 iPhone 8과 실행 대상(run destination)인 iPhone 12 Pro가 표시된다:
Preview of iPhone 8 and iPhone 12 Pro 작은 iPhone에 더 잘 맞지만(tighter fit), 모든 미리 보기에 적절하게 보인다.
"iPhone 8"코드를 복사하여 붙여넣고(copy and paste), 기기(device)의 이름을 다음과 같이 변경한다:
.previewDevice("iPhone 12 Pro Max")
이제 세 개의 미리보기가 있다. 그리고 디자인 요소(design elements)의 크기가 알맞게(fit) 조정(resized)되었다.
Key points
- SwiftUI 뷰(views)와 수정자(modifiers)를 사용해 설계(design) 아이디어(ideas)를 신속하게(quickly) 구현(implement)할 수 있다.
- 라이브러리(Library)에는 기본(primitive) 뷰(views)와 수정자(modifier) 메서드(methods) 목록(list)이 포함(contains)되어 있다. 사용자 정의(custom) 뷰(views), 버튼 스타일(button styles), 수정자(modifiers)를 쉽게 만들 수 있다.
- 뉴포피즘(neumorphism)은 새로운 스큐어모피즘(skeuomorphism, 있는 그대로의 모습을 사실적으로 표현하는 디자인 기법)이다. 색상 세트(color sets)와 SwiftUI
shadow
수정자(modifier)를 사용하여 쉽게 구현(implement)할 수 있다. ZStack
을 사용하여 UI 요소(elements)를 계층화(layer)할 수 있다. 예를 들어(for example), 배경색(background color)을 배치(lay down)하고 안전 영역(safe area)으로 확장(extend)한 다음 나머지(rest) UI를 여기에 배치(layer)한다.- 일반적으로(usually) 뷰를 채우거나(fill) 테두리(border)를 감싸기(wrap) 전에 뷰(view)의 레이아웃(layout)이나 위치(position)를 변경(changes)하는 수정자(modifier)를 적용(apply)한다.
- 일부 수정자(modifiers)는 모든 뷰(view) 유형(types)에 적용(applied to)할 수 있는 반면, 다른 수정자는
Text
또는Shape
과 같은 특정(specific) 뷰(view) 유형(types)에만 적용(applied)할 수 있다. 모든Text
수정자(modifiers)가Text
뷰(view)를 반환(return)하는 것은 아니다. makeBody(configuration:)
메서드(method)를 구현(implementing)하여 사용자 정의(custom) 버튼 스타일(button style)을 만든다. 탭(tapped)하면 레이블 색상(label color)과 조광(dimming)과 같은 일부 기본 동작(default behavior)이 손실된다.- 미리보기(preview)에 예상(expect)한 내용이 표시되지 않는 경우, 보이지 않는 문제(phantom problem)를 해결하는 데 시간을 낭비하기 전에 시뮬레이터(simulated)나 실제 기기(real device)에서 실행(running)해 본다.
GeometryReader
를 사용하여 기기(device)의 프레임(frame)과 크기(size) 속성(properties)에 접근(access)한다.
'Raywenderlich > SwiftUI by Tutorials' 카테고리의 다른 글
Chapter 6: Controls & User Input (0) 2021.04.07 Chapter 5: Intro to Controls: Text & Image (0) 2021.04.07 Chapter 4: Testing & Debugging (0) 2021.04.05 Chapter 2: Getting Started (0) 2021.03.17 Chapter 1: Introduction (0) 2021.03.17