스위프트3을 접하고 몇 년이 지나고 ... 정들었던 UIkit의 자리를 swiftUI가 점점 뺏고 있는와중

나도 swiftUI를 공부해야겠다 싶어 시작한다.

uikit의 내용을 swiftUI와 대조하며 파악해보려한다.

이글은 내 개인의 학습하면서 쓰는 글이라 틀린 정보가 있을 수 있다.

UIWindow -> windowGroup

WindowGroup {
    ContentView()
}


UILabel ->

Text("hello world")


UIButton ->

init(action: @escaping () - Void, @ViewBuilder label: () -> Label)

var body: some View {
    Button {
        print("Tap")
    } label: {
       Text("Tap me!")
    }
}


UITextField ->

init(LocalizedStringKey, text: Binding<String>)

@State private var textValue: String = ""

var body: some View {
    TextField("PlaceHolder...", text: $textValue)
}


UIImageView ->

init(_ name: String, bundle: Bundle? = nil)

var body: some View {
    Image(systemName: "star") // systemImage
}


UITableView ->

init(@ViewBuilder content: () -> Content)

var body: some View {
    List {
        ForEach(0 ..< 10, id: \.self) { i in
            Text("item \(i)")
        }
    }
}


UICollectionView ->

ScrollView + 
init(columns: [GridItem], alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, pinnedViews: PinnedScrollableViews = .init(), @ViewBuilder content: () -> Content)

let layout: [GridItem] = [
    GridItem(.flexible(minimum: 40, maximum: 100)),
    GridItem(.flexible(minimum: 40, maximum: 100)),
]

var body: some View {
    VStack(alignment: .center) {
        ScrollView(.vertical) {
            LazyVGrid(columns: layout) {
                ForEach(0 ..< 100, id: \.self) { index in
                    Text("item \(index)")
                        .frame(height: 30)
                }
            }
        }
        .frame(maxHeight: .infinity)
        .background(.yellow)

        ScrollView(.horizontal) {
            LazyHGrid(rows: layout) {
                ForEach(0 ..< 100, id: \.self) { index in
                    Text("item \(index)")
                        .frame(height: 30)
                }
            }
        }
        .frame(maxHeight: .infinity)
    }
}


UIAlertController style.alert->

View().alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View
View().alert<A>(_ titleKey: LocalizedStringKey, isPresented: Binding<Bool>, @ViewBuilder actions: () -> A) -> some View where A : View

@State private var showingAlert = false

var body: some View {
    Button(action: {
        self.showingAlert = true
    }) {
        Text("Show Alert")
    }
    .alert("title", isPresented: $showingAlert) {
        Button("OK", role: .cancel) {
            print("tap ok")
        }
        Button("NO", role: .destructive) {
            print("tap no")
        }
    }
    /*
    .alert(isPresented: $showingAlert) {
        Alert(title: Text("Title"), message: Text("This is a alert message"), dismissButton: .default(Text("Dismiss")))
    }
    */
}


UIAlertController style.actionSheet

View().actionSheet(isPresented: Binding<Bool>, content: () -> ActionSheet) -> some View

@State var showActionSheet = false
var body: some View {
    Button("sheet") {
        showActionSheet = true
    }
    .buttonStyle(.bordered)

    .actionSheet(isPresented: $showActionSheet) {
        ActionSheet(title: Text("Title"), buttons: [
            ActionSheet.Button.default(Text("text"), action: {
                print("tap text")
            }),
            .default(Text("default")),
            .cancel()
        ])
    }
}


UISwitch ->

init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label)

@State private var toggleValue: Bool = false

var body: some View {
    Toggle(isOn: $toggleValue) {
        Text("토글 텍스트")
    }
}


UIPickerView + UISegmenetedControl( style segmented ) ->

init(_ titleKey: LocalizedStringKey, selection: Binding<SelectionValue>, @ViewBuilder content: () -> Content)
init(selection: Binding<SelectionValue>, label: Label, @ViewBuilder content: () -> Content)

@State var select = "Apple"

var fruits = ["Apple", "Apricot", "Banana", "Pear"]

var body: some View {
    VStack(alignment: .leading) {
        Picker("picker 타이틀", selection: $select) {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
        }
        .pickerStyle(.inline)
        .background(.red)

        Picker(selection: $select, label: Text("Fruit")) {
            ForEach(fruits, id: \.self) {
                Text($0)
            }
        }
        .background(.green)

        NavigationView {
            Picker(selection: $select, label: Text("Fruit")) {
                ForEach(fruits, id: \.self) {
                    Text($0)
                }
            }
            .pickerStyle(.navigationLink)
            .background(Color.mint)
        }

        Picker("title", selection: $select) {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
        }
        .pickerStyle(.segmented)
        .background(Color.orange)

    }.padding()
}


UIViewcontroller().present ->

View().sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View
View().fullScreenCover<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View

struct ModalView: View {
    @Binding var isShow: Bool
    var body: some View {
        ZStack {
            Color.yellow.ignoresSafeArea()
            VStack {
                Button("close") {
                    isShow = false
                }
                .frame(height: 50)

                Text("hello world")
            }
            .background(.green)
        }
    }
}

struct TestPresent: View {
    @State private var fullShow = false
    @State private var modalShow = false

    var body: some View {

        VStack {
            Button("Present0") {
                self.fullShow = true
            }
            .padding()

            Button("Present1") {
                self.modalShow = true
            }
            .padding()

            .sheet(isPresented: $modalShow) {
                ModalView(isShow: $modalShow)
            } // 아직도 애네들이 어떻게 붙어서 동작하는지 이해가 안간다.
        }
        .fullScreenCover(isPresented: $fullShow) {
            ModalView(isShow: $fullShow)
        } // 도대체 넌 어떻게 붙어서 동작하는거니 안에 View 아무곳에 붙여도 동작함
    }
}

\

기존 pod을 이용한 프로젝트에서 tuist와 spm을 이용해 모듈화를 하고있다 

서비스 분리 자체를 완전히 하지는 않았지만 common과 main 두가지 정도로 분리하여 나중에 서비스를 추가할때 하나씩 늘려가는식으로 진행하고 좀 더 시간이 많이 남을때 main에서의 서비스를 분리하는식으로 진행하려고 했었다

그렇게 진행한 부분을 업데이트를 하려고 업로드를 시켜보니...

Asset validation failed

Missing App Icon. The bundle doesn’t contain an iMessage app icon. iMessage app icons must be 54x40 pixels in .png format. (ID: 3f448328-3fc2-4036-be0e-7d0eacf9ca41)

주루루룩.

Asset validation failed 가 뜨면서 올라가지 않는다.

achive까진 됐는데 업로드가 안된다.

이전까지 멀쩡히 올라가던 앱이라 이미지가 부족할리도 없다

해당 사이즈는 아이콘으로 요구하지도 않는 이미지다

왜 저런 에러가 나왔는지도 의문이다

 

tuist로 옴기면서 글은 안남겼지만 가장 많이 나는 오류에 대한 해답은 tuist 프로젝트 구성 정의에 대한 문제였다

이번에도 문제가 있겠거늘 하고 검색도 해보고 여러가지도 해봤지만 영 안나왔고 결국 찾아낸 문제는

기존 프로젝트의 Build Settings의 Asset Catalog Compiler - Options의 값이 다르게 나온다.

tuist가 생성해주는 기본값과 다른가보다

Target을 생성할때

setttings를 항목이 있는데 이를 다시 기존 프로젝트 설정과 동일하게 적어주고

( 참고로 Target -> Build Settings 에서 원하는 항목을 클릭하면 우측 메뉴에서 Declaration 라고 된 부분에서 키값이 나온다. )

tuist generaral > 이후 혹시 모르니 빌드 클린 > achive > upload

완료!

 

tuist 로 생성되는 기본값이 너무나 적다 힘들다

 

다른 라이브러리 문제가 생겨 lock파일을 버리고 새로 업데이트좀 했더니

구글 signin라이브러리가 에러를 내뱉는다.

signIn(with) 이 없단다

이 무슨 개소린가 하고 공식 문서를 살펴보지만 안보인다 swiftUI 문서가 생기기전에 만들어놓은 소스들이라 그런가 싶다

공식문서에서 말해주는데로 다시 짜본다

 

https://developers.google.com/identity/sign-in/ios/reference/Classes/GIDSignIn#-signinwithpresentingviewcontroller:hint:additionalscopes:completion:

func signIn(withPresenting presentingViewController: UIViewController, hint: String?, additionalScopes: [String]?) async throws -> GIDSignInResult

 

이게
GIDSignIn.sharedInstance.signIn(with: config, presenting: vc) { user, error in }

 

config설정과 presentingvc설정을 해줘야한다.

config를 넣어서 하는 함수는 보이지 않는다

sharedInstance.configuration 항목이 셋업도 할 수 있는거 같아 여기에 넣어줬다

GIDSignIn.sharedInstance.configuration = .init(clientID: clientID)

GIDSignIn.sharedInstance.signIn(withPresenting: vc){ result, error in

//에러처리 로그인 실패 처리

// result는 GIDSignInReuslt 라 기존에 idToken을 뽑아내려면 좀더 들어가야있더라

let idToken = result.user.idToken.tokenString

// idToken이 기존에는 그냥 String으로 줬는데 IDToken이라는게 또 따로있더라

// 로그인 성공 처리

}

cocoapod으로 Snapkit 5.6.0 버전을 다운받아 사용중이였는데

작업 pc변경이후 계속 모듈을 찾을 수 없다는 빌드 에러가 떠서 곤란하게되었다.

실제로 다운받은 파일을 확인해보면 pod으로 받은 다른 파일들은 확인되나 Snapkit만이 폴더만 생성되고 라이브러리의 정보만 담은 파일이 있을뿐 실 코드들이 없다.

이 문제때문에 우선적으로 많이 하는 해결책으로

clean build folder를 돌려보았고.

pod로 받는 파일들을 새롭게 받아보아보았고

DerivedData 폴더를 비워보았으나 역시나 되지 않는다.

 

이런 문제로 사용한 해결방법 아래와같다

1. pod repo update ( podsepc 업데이트 ) 

2. clean build folder ( 기본 단축키 cmd + shift + k )

3. Podfile.lock 삭제

4. Pods폴더 삭제

5. DerivedData 폴더 내부 비움 ( 설정된 DerivedData 폴더의 경로는 xcode의 Preferences... -> Locations 에서 Derived Data 항목으로 알 수 있다.

6. pod install

이제 다시 Pods의 Snapkit 폴더를 찾아보면 무사히 코드들이 다운받아졌음을 알 수 있다

 

 

GADErrorCode

/// The ad request is invalid. The localizedFailureReason error description will have more
/// details. Typically this is because the ad did not have the ad unit ID or root view
/// controller set.
InvalidRequest = 0,

/// The ad request was successful, but no ad was returned.
NoFill = 1,

/// There was an error loading data from the network.
NetworkError = 2,

/// The ad server experienced a failure processing the request.
ServerError = 3,

/// The current device's OS is below the minimum required version.
OSVersionTooLow = 4,

/// The request was unable to be loaded before being timed out.
Timeout = 5,

/// The mediation response was invalid.
MediationDataError = 7,

/// Error finding or creating a mediation ad network adapter.
MediationAdapterError = 8,

/// Attempting to pass an invalid ad size to an adapter.
MediationInvalidAdSize = 10,

/// Internal error.
InternalError = 11,

/// Invalid argument error.
InvalidArgument = 12,

/// Received invalid response.
ReceivedInvalidResponse = 13,

/// A mediation ad network adapter received an ad request, but did not fill. The adapter's error
/// is included as an underlyingError.
MediationNoFill = 9,

/// Will not send request because the ad object has already been used.
AdAlreadyUsed = 19,

/// Will not send request because the application identifier is missing.
ApplicationIdentifierMissing = 20,

GADPresentationErrorCode

/// Ad isn't ready to be shown.
AdNotReady = 15,

/// Ad is too large for the scene.
AdTooLarge = 16,

/// Internal error.
Internal = 17,

/// Ad has already been used.
AdAlreadyUsed = 18,

/// Attempted to present ad from a non-main thread.
NotMainThread = 21,

/// A mediation ad network adapter failed to present the ad. The adapter's error is included as an
/// underlyingError.
Mediation = 22,

 

 

에러 도메인

"com.google.admob"

이전 시큐리티기능을 사용하지 않고 따로 히든텍스트라든 변수를 정의해줘서 비밀번호 가리고 보이고하는 기능을 구현해놨었다

해당 스크린을 테스트하기위해 테스트코드를 짜놨었는데....

 

이번 최신 버전을 배포하던중 텍스트필드에 중대한 버그가 발견되어 텍스트필드의 시큐리티 기능을 이용하는 방식으로 전환했다

그리고.. 그 이후로 UI테스트코드를 통과하지 못하고있다

도저히 잘못된점을 모르겠어서 뷰를 하나하나 스택쌓아가듯이 찾아 나아가보지만 여전히 해당 아이디를 가진 텍스트필드를 찾을 수 없다고 오류를 뱉고있었다.

 

나의 이 멍청한 시도를 다른이는 하지 않기를 기도하며 문제점을 여기에 적는다

나는 해당 부분을 이렇게 정의하며 찾아보고있었다.

- 라인2 오류 -

let loginvcContentView = app.otherElements["loginvcScrollViewContentView"]
let pwTextField = loginvcContentView.textFields["loginvcPwTextField"]

UITest에 대해 누가 알려주지 않아 거의 그냥 막코딩이라 변수이름이라던가 방법은 그냥 넘어가줬으면 좋겠다

문제는 loginvcContentView에서 찾는 엘레먼트 타입이였다.

텍스트필드에

textField.isSecureTextEntry = true

라고 정의를 해주면 textFields로는 찾을 수 없고

아래와 같이 해줘야한다.

let loginvcContentView = app.otherElements["loginvcScrollViewContentView"]
let pwTextField = loginvcContentView.secureTextFields["loginvcPwTextField"]

그렇다 

저 속성하나 켜줬다고 textField가 아닌 secureTextFields로만 찾을 수 있게되었다.

망할

 

'iOS > swift' 카테고리의 다른 글

No such module ~~ 설치되지 않는 Snapkit 문제  (0) 2023.04.12
ios - Admob 에러코드 메모  (0) 2023.03.21
Alamofire.AFError Code  (0) 2022.10.06
Block Based KVO, iOS - contentSize  (0) 2022.09.27
Date 끼리의 비교  (0) 2022.08.04

'Alamofire', '~> 5.2'

code name
0
createUploadableFailed
1
createURLRequestFailed
2
downloadedFileMoveFailed
3 invalidURL
4
multipartEncodingFailed
5
parameterEncodingFailed
6
parameterEncoderFailed
7
requestAdaptationFailed
8
requestRetryFailed
9
responseValidationFailed
10
responseSerializationFailed
11
serverTrustEvaluationFailed
12
sessionInvalidated
13
sessionTaskFailed
14
urlRequestValidationFailed
15
explicitlyCancelled
16
sessionDeinitialized

 

'iOS > swift' 카테고리의 다른 글

ios - Admob 에러코드 메모  (0) 2023.03.21
XCTest, textFields["passwordTextField"] 는 찾을 수 없다.  (0) 2022.10.31
Block Based KVO, iOS - contentSize  (0) 2022.09.27
Date 끼리의 비교  (0) 2022.08.04
bringSubviewToFront(_:)  (0) 2022.06.29

기존 스크롤뷰의 컨텐츠 사이즈가 변경될때마다 작업을 해야하는게 있어서 어떻게 해줄까 고민하다 

스크롤뷰에

horizentalScrollView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)

해주고  observeValue 를 override해줘서 받을 수 있다는 내용 그대로 따라해 준적이 있다.

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    if let obj = object as? UIScrollView, obj == self.horizentalScrollView && keyPath == "contentSize" {
    // 작업 내용
    }
}

 

 오브젝트에 컨텐츠 사이즈 라는 키로 .new 될때마다 옵저브 벨류에서 받아서 해줄수 있구나 라며 기뻐했건만

Lint 작업을 추가하면서 이걸 변경해야되는 일이 생겼다.

Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo)

라며 이건 옛날 스타일이라며 나를 갈구는 xcode의 메시지에 이걸 그냥 비활성화 해줄까 고민하다 방법을 찾았다.

나는 옵저버 추가를 viewdidload에서 해줬었는데 아래와 같이 변경해줬다.

var scrollViewContentSizeObserver: NSKeyValueObservation?

override func viewDidLoad() {
...

    scrollViewContentSizeObserver = horizentalScrollView.observe(\.contentSize, options: [.new], changeHandler: { view, change in
        // 작업 내용
    })

...
}

혹시 동작을 안할까봐 걱정을 많이 했으나 시뮬레이터 상에서는 멀쩡히 돌아가서 다행이다.

솔직히 저 contentSize 집어 넣는 부분이 아직도 잘 이해가 안간다.

누가 알려줄 사람은 없나

 

 

+ Recent posts