![](https://private-user-images.githubusercontent.com/116441522/335866016-e4d92cdd-f0be-4bad-bbc0-b23c366827dc.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjA3NTkxMDcsIm5iZiI6MTcyMDc1ODgwNywicGF0aCI6Ii8xMTY0NDE1MjIvMzM1ODY2MDE2LWU0ZDkyY2RkLWYwYmUtNGJhZC1iYmMwLWIyM2MzNjY4MjdkYy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQwNzEyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MDcxMlQwNDMzMjdaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lNmNlZWZjYWYwNDMxYjc3NDczMDA3ZGU2MDMxNWY5YzVmYTVlNDM4OTNjOTE1ODM1ZjBhYmFkZWNhYjYxMzk5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.G_tpiMPxTxzLyp5el2-ePhBoGJBDTrM8GBMXyIIkpEc)
- ShopY App은 Naver Shopping Rest - ful Api 를 통해 쇼핑을 제공합니다.
- 현재 인기 있는 상품 순위를 볼수 있습니다.
- 상품을 직접 검색하여 찾을수 있습니다. (페이지 네이션 기능)
- 각 상품에 좋아요를 남길수 있습니다.
- 좋아요를 모아서 한번에 볼수 있습니다.
- 네트워크 상태를 실시간으로 감지하여, 사용자에게 네트워크 상태를 알려줍니다.
- 최소 버전 - iOS 15
5/10 ~ 5/30 ( 대략 3주 )
- SwiftUI / Combine
- MVI / Router / SingleTone /
- Realm ( Swift )
- URLSession / Kingfisher / Codable / SwiftConcurrency
- 다크모드 대응 Asset
메인 화면 | 검색 | 세부 화면 |
---|---|---|
검색 정렬 | 좋아요 | 좋아요 모아보기 |
---|---|---|
등록 | 메인 페이지 전환 |
---|---|
Swift UI 는 UIKit 과는 다르게 자체적으로 반응형을 지원하고 있으며, 양방향의 흐름이 될수 있는 MVVM 보단 사용자의 입력을 시작으로 비즈니스 로직을 거친후, View의 상태 반영 하는 단방향 흐름인 MVI 를 통해 프로젝트를 구성하였습니다.
import Combine
protocol MVIPatternType: ObservableObject {
associatedtype Intent
associatedtype StateModel
var stateModel: StateModel { get }
func send(_ action: Intent)
}
회고정리 : Swift UI + MVVM 과연 맞을까? https://velog.io/@little_tail/9ps8g62w
API를 통해 받아오는 데이터와 뷰가 사용할 모델을 분리 하여 후에 API 응답값이 바뀌거나, 또는 뷰가 사용할 내용이 바뀔 것을 빠르게 대처하기 위해 구성 하였습니다.
final class ShopItemsRepository {
private
let shopMapper = ShopEntityMapper()
private
let repository = RealmRepository()
}
////
struct ShopEntityMapper {
// API 를 통한 모델
func toEntity(_ dto: ShopItemDTOModel) -> ShopEntityModel?
// Realm 모델의 대한
func toEntity(_ likeModel: LikePostModel) -> ShopEntityModel
}
Button 과 ViewModifer 를 이용하여 버튼을
extension View {
func asButton(action: @escaping () -> Void ) -> some View {
modifier(ButtonWrapper(action: action))
}
}
struct ButtonWrapper: ViewModifier {
let action: () -> Void
func body(content: Content) -> some View {
Button(
action:action,
label: { content }
)
}
}
WWDC 2021 에 발표한 Swift Concurrency에 대해서 학습하고, 이전에는 Completion Handler 를 통해 비동기 함수를 컨트롤 하였으나, Swift Concurrency를 활용하여 비동기 코드를 동기 코드처럼 보여질수 있게, 코드가 더 가독성 좋을수 있도록 구성 하였습니다.
import Foundation
import Combine
protocol NetworkManagerType {
typealias FetchType<T: Decodable> = AnyPublisher<T,NetworkError>
static func fetchNetwork<T:Decodable>(model: T.Type, router: NaverRouter) -> FetchType<T>
static func checkReqeust<T: Decodable>(type: T.Type, router: NaverRouter) async throws -> T
static func checkURLRequest(router: NaverRouter) throws -> URLRequest
static func checkURLResponse(response: URLResponse) throws
static func decode<T: Decodable>(data: Data) throws -> T
}
struct NetworkManager: NetworkManagerType { }
extension NetworkManager {
static func fetchNetwork<T:Decodable>(model: T.Type, router: NaverRouter) -> FetchType<T> {
Future <T, NetworkError> { promiss in
Task {
do {
let result = try await checkReqeust(type: model, router: router)
promiss(.success(result))
} catch let error as NetworkError {
promiss(.failure(error))
} catch {
promiss(.failure(.unknownError))
}
}
}
.eraseToAnyPublisher()
}
}
Naver 검색 Rest API - ful 를 통해 상품명을 받아오면 (볼드 태그) 가 받아와 지는 이슈가 있었습니다. 처음에는 직접 bold 태그를 제거 하였었으나 후에 네이버 API 에서 볼드 태그가 아닌 다른 태그로 결과 값을 줄것을 대비하여
NSAttributedString.DocumentReadingOptionKey
의 옵션중 HTML 문서 형식으로 설정하여 태그를 제거하였습니다.
// Before
var rmHTMLBold: String {
let first = self.replacingOccurrences(of: "<b>", with: "")
let results = first.replacingOccurrences(of: "</b>", with: "")
return results
}
// After
extension String {
typealias ReadingOption = NSAttributedString.DocumentReadingOptionKey
typealias DocumentType = NSAttributedString.DocumentType
var rmHTMLTag: String {
guard let data = self.data(using: .utf8) else { return self }
let options: [ReadingOption : Any] = [
.documentType: DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
]
do {
let attrubuted = try NSAttributedString(
data: data,
options: options,
documentAttributes: nil)
return attrubuted.string
} catch {
return self
}
}
}
간단한 데이터를 저장하기 유용한 UserDefaults 를 구현하기 위한 코드중 중복되는 코드들이 많아 Swift 5.1 에서 추가된 propertyWrapper 를 학습하고, UserDefaults 에 적용해 보았습니다.
@propertyWrapper
struct UserDefaultWrapper<T> {
let key: String
let placeValue: T
let ofCase: UserDefaultCase
private let US = UserDefaults.standard
var wrappedValue: T
}
// UserDefaults Manager
@UserDefaultCodableWrapper(key: Key.productId.rawValue, placeValue: [])
static var productId: Set<String>
@UserDefaultWrapper(key: Key.searchHistory.rawValue, placeValue: [], ofCase: Key.searchHistory.caseType)
static var searchHistory: Array<String>
iOS 17 버전에서는 Realm 이 정상적으로 작동 하였으나, iOS 16 이하 버전으로 실행시 ” Thread 1: EXC_BAD_ACCESS (code=1, address=0x0) “ 라는 메시지 와 함께 Realm 이 동작하지 않던 이슈가 있었습니다. 해당하는 문제를 해결하기 위해 검색과, GitHub Issue Community을 활용하였으며, 사용해야 하는 렘의 모델을 직접 명시하는 방법으로 문제를 해결하였습니다.
private var realm: Realm?
static func registerRealmClass() {
let classes: [Object.Type] = [
LikePostModel.self,
ProfileRealmModel.self
]
let config = Realm.Configuration(objectTypes: classes)
Realm.Configuration.defaultConfiguration = config
}
init() {
if #available(iOS 17, *) {
// None
} else {
RealmRepository.registerRealmClass()
}
do {
let realms = try Realm()
realm = realms
print(realm?.configuration.fileURL ?? "Realm MISS")
} catch {
print("Realm Init 문제 ")
realm = nil
}
}
Swift UI 를 처음으로 도입하면서 겪은 문제로, .Infinity ( 무한대 ) 를 마치 Spacer() 와 같은 개념이라고 착각을 하고 사용하였었는데 해당 오류를 겪고 Infinity 와 레이아웃 시스템에 대해서 학습하였습니다. 문제의 해결 방법은 아래와 같이 간단 하였으나 왜 음수라고 하였는지 원인을 알아야 한다고 생각 했었습니다.
회고정리 : Swift Layout System https://velog.io/@little_tail/SwiftUILayoutSysytem
// Before
.frame(width: .infinity, height: 46)
// After
.frame(maxWidth: .infinity)
.frame(height: 46)