MapKit (20)
Dynamic Type
Dynamic Type에 대한 참고는 여기
간단하게 정리하면 설정에서 Dynamic Type을 통해 font size를 조절할 수 있다.
Accessibility Docs Dynamic Type sizes Docs PSDPFKit Blog - Dynamic Type Stats
여기도 참고하면 좋다.
우선 DummyData를 하나 만들어 주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
static var chipotle: CKRecord {
let record = CKRecord(recordType: RecordType.location,
recordID: CKRecord.ID(recordName: "BD731330-6FAF-A3DE-2592-677F9A62BBCA"))
record[DDGLocation.kName] = "Chipotle"
record[DDGLocation.kAddress] = "1 S Market St Ste 40"
record[DDGLocation.kDescription] = "Our local San Jose One South Market Chipotle Mexican Grill is cultivating a better
world by serving responsibly sourced, classically-cooked, real food."
record[DDGLocation.kWebsiteURL] = "https://locations.chipotle.com/ca/san-jose/1-s-market-st"
record[DDGLocation.kLocation] = CLLocation(latitude: 37.334967, longitude: -121.892566)
record[DDGLocation.kPhoneNumber] = "408-938-0919"
return record
}
이때 하나 중요한게 있다. 현재 강의에 있는 그대로 MockData에 recordName을 사용했는데, 이렇게 하면 Checked In을 한 부분에 대해서는 잘 나오지 않게 된다.
그러므로, 이부분을
iCloud에서 사용하는 recordName을 그대로 MockData에 사용하면 Preview에서도 보이게 된다.
그리고 Dynamic Type에 따른 Text를 보고싶을땐 지금은
이걸 통해 바로 확인이 가능하다
물론 코드를 통해서도 바로 확인이 가능하다
1
2
3
4
5
6
#Preview {
NavigationView {
LocationDetailView(viewModel: LocationDetailViewModel(location: DDGLocation(record: MockData.chipotle)))
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
}
}
Modal 배경을 바꿔준다.
1
2
3
4
viewModel.isShowingProfileModal {
Color(.black)
// 생략
}
Modal이 보여질때 뒤의 배경을 black으로하여 검게 해주었다.
이전엔 SystemBackground였는데 비교를 해보면
이렇게 차이가 난다.
이제 제대로 해보도록 한다.
1
2
3
4
5
6
7
8
9
10
11
struct DescriptionView: View {
var description: String
var body: some View {
Text(description)
.minimumScaleFactor(0.75)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal)
}
}
Description View의 frame, linelimit을 모두 지워주었다. Font를 극단적으로 키울경우에 제약이 되는 요소를 모두 제거를 했다고 보는게 맞다.
그리고 .fixedSize(horizontal: false, vertical: true)를 사용해준다.
이건 사이즈를 고정할때 쓰는 방식이다.
참고
하지만 fixedSize를 사용했을때 Description부분 내용은 다 보이지만 나머지 UI가 안보이는 문제가 있다.
Majid Jabrayilov라는 개발자가 고안한 방법을 사용한다. 바로 scrollview를 embed하는 방식이다.
View Extension에 아래 함수를 추가한다.
1
2
3
4
5
6
7
func embedInScrollView() -> some View {
GeometryReader { geometry in
ScrollView {
self.frame(minHeight: geometry.size.height, maxHeight: .infinity)
}
}
}
그리고 preview에
1
2
3
4
5
6
#Preview {
NavigationView {
LocationDetailView(viewModel: LocationDetailViewModel(location: DDGLocation(record: MockData.chipotle))).embedInScrollView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
}
}
이렇게 적용하면
전체가 ScrollView가 입혀져서 드래그가 가능해진다.
문제점
그럼 이제 이걸 실제로 적용하면 되지 않나? 라는 생각을 하게된다.
1
2
3
4
5
6
7
8
LocationListView
ForEach(locationManager.locations) { location in
NavigationLink(destination: LocationDetailView(viewModel: LocationDetailViewModel(location: location)).embedInScrollView()) {
LocationCell(location: location, profiles: viewModel.checkedInProfiles[location.id, default: []])
// 생략
}
}
LocationDetailView에 대해 embedInScrollView를 적용 해주었다.
폰트가 적당함에도 불구하고 이렇게 전체가 그대로 ScrollView가 적용이 되는 문제가 발생한다.
해결책
1
2
3
4
5
6
7
8
9
//LocationListViewModel
@ViewBuilder func createLocationDetailView(for location: DDGLocation, in sizeCategory: ContentSizeCategory) -> some View {
if sizeCategory >= .accessibilityMedium {
LocationDetailView(viewModel: LocationDetailViewModel(location: location)).embedInScrollView()
} else {
LocationDetailView(viewModel: LocationDetailViewModel(location: location))
}
}
LocationListViewModel에 다음과 같은 함수를 하나 만들어 주었다.
우리가 설정한 사이즈 기준에 따라 embedInScrollView를 적용할지 말지를 정하는 간단한 함수처럼 보이지만, 이때 @ViewBuilder를 사용하지 않고 만들게 되면
이렇게 에러가 발생한다. some View는 함수 내의 모든 리턴 경로에서 동일한 타입을 반환해야 하는데 .embedInScrollView()를 쓴 쪽과 안 쓴 쪽은 타입이 서로 달라 충돌했기 때문
그래서 이 문제를 해결하기 위해 ViewBuilder를 사용한다.
- ViewBuilder from Docs
- 정의: 클로저로부터 뷰를 구성(constructs)하는 커스텀 파라미터 속성이다.
- 핵심 역할: 자식 뷰를 생성하는 클로저 매개변수에 사용하여, 해당 클로저가 여러 개의 자식 뷰를 동시에 제공할 수 있도록 허용한다.
- 구현 로직:
- 호출부(Client)는
{ }클로저 내부에 여러 개의 실행문(multiple-statement)을 나열하여 여러 뷰를 전달할 수 있다. - 타입 일치화: 시스템은 분기문 등에 의해 서로 다른 타입이 반환될 경우, 이를 내부적으로 _ConditionalContent와 같은 하나의 결과물(통일된 타입)로 래핑하여 some View의 단일 타입 원칙을 지켜준다.
우리가 아무렇지 않게 쓰는 Stacks(Z, H, V)들도 모두 ViewBuilder를 포함하고 있다.
ZStack 설명 일부
이제 ListView에 적용을 해본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct LocationListView: View {
// 생략
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
NavigationView {
List {
ForEach(locationManager.locations) { location in
NavigationLink(destination:
viewModel.createLocationDetailView(for: location, in: sizeCategory)) {
// 생략
}
}
.onAppear {
// 생략
}
}
// 생략
}
}
}
실행해보면 평상시엔 문제가 없다. (사진 생략)
이제는 사이즈에 따라 적용이 잘 되는걸 알 수 있다.
똑같이 LocationMapView에도 적용한다.
1
2
3
4
5
6
7
.sheet(isPresented: $viewModel.isShowingDetailView) {
NavigationView {
viewModel.createLocationDetailView(for: locationManager.selectedLocation!, in: sizeCategory)
// 생략
}
.accentColor(.brandPrimary)
}
size에 따라 Columns 다르게 하기
이렇게 사이즈가 커졌을때를 대비하여 행을 관리를 해보려 한다.
1
2
3
4
5
6
7
8
9
10
11
// LocationDetailVM
func determineColumns(for sizeCategory: ContentSizeCategory) -> [GridItem] {
let numberOfColumns = sizeCategory >= .accessibilityMedium ? 1 : 3
return Array(repeating: GridItem(.flexible()), count: numberOfColumns)
}
// LocationDetailView
LazyVGrid(columns: viewModel.determineColumns(for: sizeCategory)) {
// 생략
}
이렇게 해주었다.
그리고
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct FirstNameAvatarView: View {
@Environment(\.sizeCategory) var sizeCategory
var profile: DDGProfile
var body: some View {
VStack {
AvatarView(image: profile.createAvatarImage(),
size: sizeCategory >= .accessibilityMedium ? 100 : 64)
// 생략
}
}
}
AvartarView 역시도 사이즈를 다르게 해주었다.
적용된 사진은 패스
Github: Dub-Dub-Grub Repository