Android Studio로 Flutter 개발 도중 아이폰으로 실행 시 하얀 화면만 출력되었다. 우선 Flutter로 개발 시 기기 실행은 Release 모드만 가능하다고 메세지가 출력되고 네트워크 관련 오류 메세지가 출력된다. 이를 해결 하기 위한 방법으로 flutter 문서의 아래 링크를 참고했다.

 

https://docs.flutter.dev/development/add-to-app/ios/project-setup#local-network-privacy-permissions

 

Integrate a Flutter module into your iOS project

Learn how to integrate a Flutter module into your existing iOS project.

docs.flutter.dev

 

1. Info.plist 파일 분기 처리

기존의 Info.plist 파일을 Info-Debug.plist 로 수정한 뒤 Info-Release.plist 라는 복사본을 만든다.

 

2.  Info-Debug.plist 에서 필요한 키 값을 추가한다.

총 2개를 추가하는데

Bonjour services - { Item 0 : _dartobservatory._tcp }

Privacy - Local Network Usage Description - "표시 될 메세지"

를 추가하면 된다.

 

3. 프로젝트의 Info.plist 경로 변경

위에서 기존의 Info.plist의 이름을 변경하고 분리하였으니 이에 대한 설정을 추가해준다.

Packaging - Info.plist File 항목을 찾은 뒤

우리가 변경했던 plist 파일의 이름으로 변경해준다. 문서에서는 path/to/Info-$(CONFIGURATION).plist 로 변경하게 되면 자동으로 처리된다고 했지만 문서에서는 Profile이 존재하지 않는 버전인것 같다. 나는 Profile에 관련된 plist를 만들지 않았기 때문에 release로 입력해주었다.

 

4. Info-Release.plist 복사본이 리소스 복사 빌드 단계에 있는 경우 제거

build Phases - Copy Bundle Resources 로 이동한 뒤

위 항목중에 Info-Release.plist 가 포함되어 있는 경우 해당 항목을 제거해준다.

 

 

 

위 절차로 진행하게 되면 굳이 Xcode를 실행한 후 release모드로 변경 후 실행할 필요 없이 Android Studio 에서 바로 아이폰 기기에서 앱을 실행할 수 있게 된다.

반응형

프로젝트 생성 이후 실행에는 문제없지만 아래 에러들이 생겼다.

 

Cannot resolve symbol 'Properties'

Cannot resolve symbol 'GradeException'

 

먼저 File > Project Structure 로 들어간 후

 

좌측 메뉴에서 Modules에 프로젝트명_android가 빨간줄로 표시되어 있는 것을 확인 할 수 있다. 해당 부분에서 Dependencies 항목에 들어간 후 Module SDK 가 No SDK로 되어있는 부분을 올바른 SDK를 선택해주면 해결 가능하다.

 

이 이후에 Cannot resolve symbol 'Properties' 의 에러는 사라졌지만 Cannot resolve symbol 'GradeException' 에러는 여전히 존재한다. GradleException는 Android API 29 부터 FileNotFoundException으로 대체되었기 때문에 직접 수정해주면 모든 에러가 사라진다.

반응형

actionSheet

 

ActionSheet 를 사용하는데 deprecated 되었다고 confirmationDialog를 사용하라고 합니다. 바로 알아봅시다.

 

ConfirmationDialog

가장 기본적인 confirmationDialog 입니다. 첫번째 인자로 titleKey가 들어가고 isPresented 에 바인딩값을 연결해준 뒤 action 클로저 안에 우리가 평소에 쓰는 Button을 쭉 나열하면 됩니다.

 

struct ContentView: View {
    @State var isPresented = false
    
    var body: some View {
        Button(action: {
            isPresented = true
        }) {
            Text("사진 불러오기")
                .padding()
        }
        .confirmationDialog(
            "제목",
            isPresented: $isPresented,
            actions: {
                Button("카메라") {
                    print("카메라 선택")
                }
                Button("라이브러리") {
                    print("라이브러리 선택")
                }
            }
        )
    }
}

기본적으로 Cancel 버튼이 존재하고 제목이나 메세지가 생략이 가능합니다.

 

메세지 추가

.confirmationDialog(
            "제목",
            isPresented: $isPresented,
            actions: {
                Button("카메라") {
                    print("카메라 선택")
                }
                Button("라이브러리") {
                    print("라이브러리 선택")
                }
            },
            message: {
                Text("불러올 사진 위치를 선택해주세요.")
            }
        )

제목 추가

세번쨰 인자로 존재하는 titleVisibility 를 통하여 설정 가능합니다.

.confirmationDialog(
    "제목",
    isPresented: $isPresented,
    titleVisibility: .visible,
    actions: {
        Button("카메라") {
            print("카메라 선택")
        }
        Button("라이브러리") {
            print("라이브러리 선택")
        }
    },
    message: {
        Text("불러올 사진 위치를 선택해주세요.")
    }
)

ActionSheet.Button의 .destructive

.destructive 의 설정은 Button을 생성할 때 할 수 있습니다.

Button("카메라", role: .destructive) {
    print("카메라 선택")
}

 

확실히 기존의 ActionSheet 보다 더욱 더 선언형에 맞게 변화된 모습이 보이네요. 사용하기에도 더욱 편한것 같습니다.

반응형

SwiftUI의 Preview는 따로 앱을 빌드하여 시뮬레이터를 실행하지 않고도 코드를 입력하면 바로바로 눈에 보이도록 해주는데요. 이로 인해 더욱 더 생산성 높은 작업을 할 수 있도록 도와줍니다. SwiftUI에서 하나의 ViewController 같은 화면이 아닌 하나의 Component View를 만드는 경우가 많습니다. 하지만 이럴 때도 Preview는 선택한 기기의 화면으로 보입니다. 이런 경우나 여러가지의 형태(가로모드, 세로모드) 등등 을 한눈으로 확인 할 수 있도록 해주는 Preview의 기능에 대해 알아봅시다.

 

1. PreviewLayout

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(.device)
        ContentView()
            .previewLayout(.fixed(width: 400, height: 200))
        ContentView()
            .previewLayout(.sizeThatFits)
    }
}

previewLayout은 말 그대로 preview 화면의 레이아웃에 관련된 함수입니다. 의미도 모두 직관적이라 따로 설명은 생략하도록 할게요!

위 코드 처럼 View 타입을 여러개 선언해주게 되면 아래 이미지처럼 한번에 여러개의 Preview를 볼 수 있습니다. Component View를 작성할 때 .sizeTahtFits을 자주 사용하고 있습니다.

 

 

previewLayout

 

 

2. PreviewDevice, PreviewDisplayName

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 12 mini"))
            .previewDisplayName("iPhone 12 mini")
        ContentView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 13 Pro Max"))
            .previewDisplayName("iPhone 13 Pro Max")
    }
}

 먼저 previewDevice는 아이폰 기기의 이름으로 preview의 디바이스를 직접 변경시켜 줄 수 있습니다. Xcode에서 시뮬레이터를 실행할 때 기기를 하나만 선택하여 하나의 기기로 실행하게 되는데 이를 이용하면 해당 화면이 여러 화면에서 어떻게 동작되는지 확인할 수 있습니다. PreviewDevice의 enum을 인자로 받게 되어있는데 해당 기기들이 case로 정해져 있지 않고 저렇게 String값으로 생성해줘야 한다는게 좀 불편하네요... 자주 사용하시는 분들이라면 따로 enum을 정의해도 좋을 것 같네요!

 

 PreviewDisplayName은 Preview에 이름을 붙혀줍니다. 아래 사진을 보면 기기 위에 정의해둔 이름이 붙혀인 것을 보실 수 있습니다.

previewDevice, previewDisplayName

 

 

3. Preview 가로모드로 보는법

아쉽게도 Preview에서는 가로모드가 지원되지 않습니다. 하지만 위에서 사용되었던 previewLayout을 사용하면 됩니다!

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewLayout(.fixed(width: 1180, height: 820))
    }
}

아이패드 가로모드

 

반응형

일반적으로 Assets에 고정된 색상을 등록하고 사용하지만 가끔은 서버에서 받은 hex값을 사용하여 색상을 표현해야 하는 경우가 있다.

역시 일반적인 방법으로 Color를 Extension 하여 새로운 이니셜라이저를 추가해준다.

extension Color {
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&int)
        let a, r, g, b: UInt64
        switch hex.count {
        case 3: // RGB (12-bit)
            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6: // RGB (24-bit)
            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8: // ARGB (32-bit)
            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (a, r, g, b) = (1, 1, 1, 0)
        }

        self.init(
            .sRGB,
            red: Double(r) / 255,
            green: Double(g) / 255,
            blue:  Double(b) / 255,
            opacity: Double(a) / 255
        )
    }
}

 

출처: https://stackoverflow.com/questions/56874133/use-hex-color-in-swiftui

반응형

iOS 의 키보드를 보면 기본적으로 키보드를 내리는 키가 있지만 많은 이용자들이 화면을 탭했을 때 키보드가 내려가는 동작이 익숙하다. 아주 간단하게 SwiftUI에서 구현하는 방법이 있다.

 

1. Extension 선언

extension UIApplication {
    func addTapGestureRecognizer() {
        guard let window = windows.first else { return }
        let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
        tapGesture.requiresExclusiveTouchType = false
        tapGesture.cancelsTouchesInView = false
        tapGesture.delegate = self
        window.addGestureRecognizer(tapGesture)
    }
}

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // set to `false` if you don't want to detect tap during other gestures
    }
}​

 

2. 사용

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
        }
    }
}

위와 같이 최상단의 View에 onAppear를 호출하는것으로 모든 화면에서 해당 동작이 적용된다.

 

출처: https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui

반응형
extension Bundle {
    var appName: String {
        return infoDictionary?["CFBundleName"] as! String
    }

    var bundleId: String {
        return bundleIdentifier!
    }

    var versionNumber: String {
        return infoDictionary?["CFBundleShortVersionString"] as! String
    }

    var buildNumber: String {
        return infoDictionary?["CFBundleVersion"] as! String
    }
}

사용할 때에는 아래와 같이 사용합니다.

let str = "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))"

 

출처: https://stackoverflow.com/questions/25965239/how-do-i-get-the-app-version-and-build-number-using-swift

반응형

보통 Git이나 Cocoapods 등 terminal을 켜서 특정 작업을 진행한 뒤 다시 Xcode를 실행하기 위해 Finder를 다시 열거나 Xcode를 실행한다. 만약 Cocoapods을 사용하는 프로젝트인 경우 .xcworkspace 파일을 실행해야 하고 그렇지 않은 경우 .xcodeproj 을 실행하게 된다. 

open MyProjectName.xcodeproj

open MyProjectName.xcworkspace

위 명령어를 입력해 Cocoapods의 사용 유무에 따라 직접 입력해 줄 수도 있다. 하지만 xed 명령어를 사용하면 더욱 더 생각할 필요 없이 바로 실행이 가능하다.

 

Xed 사용

xed .

해당 명령어를 입력하면 만약 xcworkspace 파일이 존재할 경우에는 해당 파일을 우선적으로 실행하고 그렇지 않은 경우 xcodeproj파일을 실행하게 된다. 즉 Cocoapods의 사용 유무를 전혀 고려할 필요도 없다!

 

이제 프로젝트 파일을 다시 열기전에 open ... 이라던지 터미널을 끄고 Xcode 아이콘을 눌러 실행해준다던지 할 필요가 전혀 없다!!

반응형

'ios > Xcode' 카테고리의 다른 글

Xcode 13.2 - Internal error: missingPackageDescriptionModule  (0) 2021.12.18

1. 왜 SwiftUI의 Alert이 아닌 UIKit의 Alert을 사용하는가?

SwiftUI의 Alert

먼저 SwiftUI에서 제공되는 Alert을 사용하기 위한 필요한 요소에 대해 알아보자.

1. alert이 열려있는지 닫혀있는지에 대한 정보를 담은 Bool 타입의 State 변수를 선언한다.

2. View에 alert을 체이닝 형식으로 선언한 후 아까 선언한 State 변수를 연결한다.

3. View의 alert 클로저에 Action처리를 포함한 Alert(View)을 추가한다.

struct AView: View {
	// 1
    @State var isShowingAlert = false
    
	var body: some View {
    	VStack {
        	Button(action: { 
            	isShowingAlert = true
            }) {
            	Text("show alert")
            }
        }
        .alert(isPresented: $viewModel.showingPayAlert) { 	// 2
            // 3
            Alert(
                    title: Text("알림"),
                    message: Text("수락하시겠습니까?"),
                    primaryButton: .default(Text("확인")) {
                        print("action!")
                    },
                    secondaryButton: .cancel(Text("취소")
            )
        }
    }
}

 SwiftUI에서 제공되는 Alert을 사용하려면 해당 Alert이 열림과 닫힘을 알 수있는 State 변수 하나, View타입 끝에 체이닝 형식으로 alert을 하나하나 달아줘야 한다. 만약 여러개의 화면에 모두 각각 alert이 필요하면 매번 State 변수를 만들고 매번 alert을 달아준 다음 해당 action을 추가해야한다. 물론 최상단의 View에 하나의 alert을 달아놓고 매번 최상단의 alert의 binding 변수를 변경해주거나 하는 방법으로 해결할 수 있지만 최상단의 State값이 변경됨으로써 생기는 사이드이펙트들을 또한 신경써줘야 하는 단점이 있었다. 또한 하나의 View에 여러개의 alert이 필요한 경우 하나의 View 타입에 여러개의 alert을 달아주는 경우 정상적으로 동작하지 않는 경우가 생겨 불필요한 코드들이 추가되기도 한다.

 물론 선언형인 SwiftUI에서 어울리는 방법이긴 하다. 하지만 개인적으로 모든 View에서 매번 저 3가지의 작업을 한다는게 번거롭고 코드의 양도 쓸데없이 커진다는 느낌을 받았다.

 

SwiftUI에서 UIKit

특정 Action을 처리하는 alert인데 굳이 명령형으로 하지 않고 선언형을 사용 할 필요가 있을까?

아래 코드처럼 UIKit를 사용한 AlertController를 만들어 단지 호출만 하여 간단 명료하게 Alert을 표시할 수 있다.

struct BView: View {
	var body: some View {
    	Button(action: { 
        	AlertController.showAlert(
       			title: "알림",
			message: "승인하시겠습니까?",
              	  	primaryAction: { print("승인") }
            )
        }) {
        	Text("show alert!")
        }
    }
}

아래 코드는 가장 상위의 ViewController를 찾은 후 해당 ViewController에 Alert을 추가하는 코드이다.

UIKit의 UIAlertController를 현재 Scene의 가장 상위의 ViewController를 가져와 추가하는 작업이다.

import Foundation
import UIKit

final class AlertController {
    static func showAlert(
        title: String = "알림",
        message: String?,
        primaryAction: @escaping () -> ()
    ) {
        let alert = UIAlertController(
            title: title,
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(
            .init(
                title: "확인",
                style: .default,
                handler: { _ in
                    primaryAction()
                }
            )
        )
        alert.addAction(
            .init(
                title: "취소",
                style: .cancel
            )
        )
        rootController().present(alert, animated: true)
    }
    
    private static func rootController() -> UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .init()
        }
        
        guard let root = screen.windows.first?.rootViewController else {
            return .init()
        }
        
        return root
    }
}

 

 

고찰

SwiftUI를 사용하다 보면 참 편한기능이 많고 생산성이 엄청 좋고 유지보수, 코드 관리가 편하다는 것을 느낀다. 하지만 무조건 장점만은 있는게 아니기 때문에 UIKit의 장점과 SwiftUI의 장점을 잘 종합적으로 사용해야 한다는 생각이 든다.

 

 

반응형

 

시작하기 전

 이제야 개발 경력이 1년이 다되어 가는 주니어 중 주니어 iOS 개발자이다. 개발 블로그를 시작한 이유도 매일매일 공부하는 흔적을 남기고 공백기가 생긴다면 그때의 나에게 조금이라도 후회를 남기기 위해 시작하였다. 하지만 생각보다 기술적인 문제보다 무언가를 글로써 정리하는 게 너무 어려웠다. 매일 조금씩 조금씩 여러 가지 방향으로 개발 공부를 어떻게 하면 효율적으로 꾸준하게 할 수 있을지 고민이었다. 또한 SwiftUI로만 개발을 해왔고 UIKit를 사용해본적이 거의 없으며 UIKit로 구현되어있는 라이브러리를 사용할 때나 꼭 필요한 경우에만 해당 기능을 그때그때 확인하고 사용해본 게 전부였다. 하지만 iOS 기본적인 lifecycle이나 기초 배경지식의 필요성을 심각하게 느끼고 있어 UIKit에 대한 공부를 해야겠다는 마음을 먹었다. 그러다 우연히 빗썸으로부터 4년 차 이하의 주니어 iOS 개발자를 대상으로 진행되는 교육 프로그램을 알게 되어 지원하게 되었다. 캠프에 지원을 하고 과제를 받았을 때 SwiftUI를 사용하면 안 되고 UIKit로만 구현해야 한다는 제한사항을 보고 걱정이 조금 있었다. 하지만 어차피 UIKit를 공부하려고 했었기에 UIKit 기반(StoryBoard 기반) 공부를 병행하며 제출 과제 프로젝트를 진행하였다. 그리고 운이 좋게 해당 캠프를 들을 수 있는 기회가 생겼다.

 

세션

 현업자를 대상으로 하는 프로그램이기 때문에 세션의 시작 시간이 19시 30분부터 22시 30분까지였다. 야곰 아카데미에서 6개월짜리의 내용들을 4주 만에 끝내는 커리큘럼이기 때문에 가벼운 주제는 빠르게 핵심적인 주제로 세션이 진행되었다. 대부분 어떠한 활동학습이 주어지며 매일 다른 캠퍼 분과 해당 활동학습을 진행하는 방식이었다. 세션의 내용은 내가 혼자서 공부를 한다면 언제까지도 모르고 지나갔을 법한 내용들이 많았고 지난 시간에 대충 훑어만 보고 넘어간 내용들에 대해 개념이 확립하는 시간이 되었다.

 

협업

 해당 교육 프로그램을 들으면서 세션을 듣는 시간 이외에 따로 정해진 캠퍼분들과 프로젝트를 진행하게 된다. 1주차에서는 UIKit 기반의 작은 프로젝트였고, 2주 차는 팀원과 협의를 통해 프로젝트의 기술 스택을 자유롭게 정할 수 있었는데 SwiftUI와 TCA 기반의 프로젝트로 진행하였다.

 나는 이번에 운이 정말 좋게 너무나도 배울점이 많은 분들과 프로젝트를 진행하게 되었다. 데이터 구조부터 첫 화면까지는 페어 프로그래밍으로 다 같이 하나의 화면을 보며 여러 가지 의견을 내며 진행을 하였는데 다른 두 캠퍼분들의 모습을 보고 지금까지 나에게 너무나도 반성을 하게 되는 계기가 되었다. 

 

멘토링

 매주 수요일과 토요일에 프로젝트에 대한 멘토링이 진행되었었는데 1주차에서는 정말 사소한 부분까지 체크를 해주셨다. 사소할수록 자주 작성되는 코드들에 대해 미처 고려하지 못했던 부분들을 집어주시면서 내가 놓치고 있었던 부분들을 알게 되었다. 2주 차부터는 새로운 프로젝트를 진행하는 데 있어서 멘토링 방식이 사소한 코드 리뷰보다는 전체적인 흐름에 대한 멘토링이 진행되었다. 프로젝트를 진행하는 데 있어서 방향성에 대한 의견이나 어떠한 문제에 직면했을 때 손쉽게 프로젝트를 진행할 수 있도록 해결방안도 제시해주셔서 막힘없는 프로젝트를 진행할 수 있었다.

 

무엇을 배우고 느꼈는가?

 평일 매일 진행되는 세션을 통해 iOS개발자로써 꼭 알아야 하는 기초지식과 개념이 정리가 되었고 현재 내가 어떤 부분의 개념이 부족한지 깨닫는 시간이 되었다. 항상 개발하면서 고민하고 시간을 많이 끌었던 적이 데이터 구조였다. 많은 예제 프로젝트들을 보면서 공부를 꾸준하게 해왔지만 이번 협업을 통해 어떠한 이유로 어떤 구조와 설계를 하게 되는지 여러 한 의견을 내고 들으며 많은 것을 배우게 되었다. 너무나도 오래 묵혀있던 게 풀린 게 너무 시원해서 팀원분들께 감사했다. 실제 업무를 SwiftUI를 사용하고 있기 때문에 TCA에 대하여 흥미를 가졌던 적이 있었지만 아직까지 SwiftUI에 대한 레퍼런스도 많이 없을뿐더러 TCA는 더욱 없었기 때문에 그 당시 대략적인 부분만 이해하고 넘어갔던 적이 있었는데 이번 프로젝트를 진행하면서 멘토 분과 팀원분들 덕분에 많은 문제들을 해결하며 많은 공부가 되었다.

 나는 이번 캠프를 진행하면서 세션, 멘토분 그리고 같이 프로젝트를 진행하였던 캠퍼분들을 통해 내가 가장 크게 배우고 느낀 것은 아무래도 나의 개발 공부에 대한 방향성이다. 내가 현재 부족한게 무엇인지 확실하게 깨달았고 앞으로 나에게 가장 필요한 공부가 무엇인지에 대해 우선순위가 정해졌다. 또한 공부를 할 때 어떻게 접근하는지, 어디까지 깊게 공부를 해야 하는지에 대한 개념이 잡혔다. 만약 이번 캠프를 참가하지 못했다면 나의 개발 인생에 많은 시간이 낭비되었을 것 같다는 생각이 든다. 4주 동안 회사를 다니며 주말도 쉬지 않고 세션, 프로젝트를 진행하면서 일정이 빡빡했지만 너무나도 보람 있고 알찬 시간이었다.

반응형

+ Recent posts