iOS2022. 12. 8. 13:37

 

애플에서 더이상 엔터프라이즈 계정을 만들어 주지 않으려고 한다고 한다.

새로 만들려고 하면 까다로운 조건을 걸어서 포기하게 하고 기존 계정같은 경우도 연장을 잘 안해주려고 한다고 한다.

그에 대한으로 스토어계정으로 등록되지 않은 앱 배포 라는 방식을 제안한다고 한다.

이렇게 등록된 앱은 스토어에 게시되지 않고 검색해도 나오지 않으며 별도로 제공된 링크를 통해서만 설치가 가능하다.

엔터프라이즈 계정은 아무런 제약없이 단말의 모든 기능을 무제한으로 사용할 수 있고 심사를 거치지 않기 때문에 애플이 허용하지 않는 목적의 앱들도 배포가 가능하기 때문에 많은 오남용 사례가 있었다고 한다.

하지만 갑자기 엔터프라이즈 계정을 모두 막을수는 없기 때문에 등록되지 않은 앱이라는 단계를 추가하여 내부적으로 링크를 통해서만 배포할 수 있는 방법을 제공하기 시작한것이다.

 

등록되지 않은 앱을 등록하면 스토어앱보다 느슨하게 심사가 이루어진다고 하며 실제로 스토어앱보다 빠르게 심사가 완료되었다.

 

등록되지 않은 앱으로 등록하려면 스토어앱 등록절차대로 진행하여 심사에 제출까지 되어 있어야 한다. 이 상태에서 제출물의 심사 메모 섹션에 등록되지 않은 앱으로 배포해야 하는 이유를 명시하고 앱을 제출한다.

 

자세한 정보는 아래 링크 참조

 https://developer.apple.com/kr/support/unlisted-app-distribution/ 

Posted by 삼스
iOS2022. 7. 29. 10:38

iOS에서 mac과 xcode가 없이도 로그를 확인할 수 있도록 작성한 로거앱을 소개하겠다.

이 앱은 안드로이드와 달리 별도의 앱을 폰에 설치하여 로그를 확인할 수 있는 방법이 iOS에서는 제공되지 않는것인지 내가 못찾는것인지 찾을수가 없어서 그냥 내가 필요해서 만든 앱이다.

 

https://github.com/samse/SwiftSocketLogger

 

GitHub - samse/SwiftSocketLogger: Swift socket logger

Swift socket logger. Contribute to samse/SwiftSocketLogger development by creating an account on GitHub.

github.com

github에서 소스를 다운받아서 빌드하여 앱을 설치해야 한다. 스토어에서 설치 가능하도록 스토어게시를 시도해볼 생각이다.

위 소스로 앱을 설치하고 실행하면 아래 처럼 로그를 확인할 수 있다.

 

 

소켓으로 로그를 수신받아 로깅을 하는 방식이며 백그라운드에서 로그를 수집할 수 있어야 하기 때문에 백그라운드앱으로 작성되었다. 백그라운드 동작 방식은 무음 mp3를 재생하는 단순하고 쉬운 방식으로 작성하였다. 이겄 때문에 스토어게시가 안될수도 있겠단 생각이 들긴 한데 리젝되면 다른 방식으로 수정해볼 예정이다.

 

이제 로거 서버에 해당하는 로거앱을 설치하였으니 로그를 발생시키기 위해서 필요한 클라이언트앱에서는 어떻게 해야 하는지 설명하겠다.

github 페이지에서도 설명하였지만 일단 로거앱의 소스중에 swiftsocket 폴더의 소스를 클라이언트 앱으로 모두 가져와야 한다.

그리고 다음의 클라이언트용 로거 클래스를 추가한다.

 

class SocketLogger : NSObject {
    enum LoggerStatus: Int { case disconnected=0, connecting, connected }
    var status: LoggerStatus = .disconnected

    func sendLogs(_ logStr: String, completion: (() -> ())?) {
        if status == .connected {
            return
        }
        status = .connecting
        print("Logger sendLogs : \(logStr)")
        let client = TCPClient(address: "127.0.0.1", port: 2532)
        switch client.connect(timeout: 10) {
          case .success:
            status = .connected
            let data = logStr.data(using: .utf8)
            let _ = client.send(data: data!)
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
                self.status = .disconnected
                if let completion = completion { completion() }
            }
          case .failure(let error):
            print("failure: \(error)")
            status = .disconnected
        }
    }
}

 

그리고 소켓 로거클래스를 사용하는 로거도 추가한다.

 

class Logger {
    static func debug(_ message: String?) {
        printLog(level: "🐛 DEBUG", message: message)
    }
    
    /// 경고 로그
    static func warning(_ message: String?) {
        printLog(level: "⚠️ WARNING", message: message)
    }
    
    /// 오류 로그
    static func error(_ message: String?) {
        printLog(level: "🚫 ERROR", message: message)
    }
    
    /// 정보 로그
    static func info(_ message: String?) {
        printLog(level: "ℹ️ INFO", message: message)
    }

    func printLog(level: String, message: String) {
        let logStr = "[\(level)] \(message ?? "")"
        self.appendLog(logStr)
    }
    
    static var logQueue = [String]()
    static let lock = NSLock()
    private static func appendLog(_ logStr: String) {
        print("\(logStr)")
        lock.lock(); defer { lock.unlock() }
        logQueue.append(logStr)
        triggerLogging()
    }
    private static func getLog() -> String? {
        if logQueue.count > 0 {
            return logQueue.first
        }
        return nil
    }
    private static func removeLog() {
        if logQueue.count > 0 {
            logQueue.removeFirst()
        }
    }
    
    static var socketLogger: SocketLogger = SocketLogger()
    private static func triggerLogging() {
        if let logStr = getLog() {
            socketLogger.sendLogs(logStr) {
                removeLog()
                triggerLogging()
            }
        }
    }
}

 

이제 앱코드내에서 다음과 같이 로거를 호출하면 SwiftSocketLogger앱에 해당 로그가 나오게 된다.

Logger.debug("my log" )
Logger.info("my info log")

아래 그림에 보는것과 같이 우상단의 디버그레벨 문구를 누르면 디버그레벨을 변경이 가능하며 좌측의 텍스트필드에 값을 입력하면 해당 값으로 로그를 필터링해서 볼수 있다.

 

 

단말만으로 디버깅을 해야 하는 상황에서 유용하게 사용할 만한 방법이라고 생각되며 잘 사용하고 있는 앱이다 다른 개발자들도 일부 불편함을 해소하는데 사용되었으면 좋겠다.

 

Posted by 삼스
iOS2022. 7. 28. 16:41

가볍고 단순한 안드로이드와 iOS를 위한 하이브리드 라이브러리를 소개하겠다.

 

iOS용

https://github.com/samse/WKBridgeKit

 

GitHub - samse/WKBridgeKit

Contribute to samse/WKBridgeKit development by creating an account on GitHub.

github.com

 

안드로이드용

https://github.com/samse/NBridgeKit

 

GitHub - samse/NBridgeKit

Contribute to samse/NBridgeKit development by creating an account on GitHub.

github.com

 

아주 가볍게 하이브리드앱을 개발할 수 있도록 해준다.

하이브리드앱은 웹에서 javascript로 native기능을 사용할 수 있게 해주는 플러그인을 제공해주어야 하는데 이 때 사용되어야 하는 javascript core는 아래 위치에서 찾을 수 있다.

 

https://github.com/samse/WKBridgeKit/tree/main/Sample/Sample/www

 

위 위치에서 sample.js를 열어보면 어떻게 플러그인을 사용가능한지 확인 할 수 있다.

 

앱정보를 읽어오는 javascript code샘플은 다음과 같다.

    nbridge.app.appInfo().then(function(result) {
        alert('app version : ' + result.version + "/" + result.build + "\napp name: " + result.name);
    }, function(error) {
        alert(error);
    })

애플 앱스토어에 샘플앱이 게시되어 있으니 한번씩 설치해서 확인해 보는것이 도움이 될것 같다.

 

https://apps.apple.com/kr/app/nbridge/id1628984500

 

‎nBridge

‎Swift기반으로 WKWebView와 Native를 연동할 수 있는 기본기능을 제공합니다. 아래 사이트에서 자세한 정보를 얻을 수 있으며 이를 기반으로 아주 가벼운 Hybrid앱을 작성할 수 있습니다. https://github.c

apps.apple.com

 

 

하이브리드 기술이 일반화가 많이 되었긴 하지만 가볍게 사용가능한 라이브러리가 있으면 좋겠다는 생각에 정리하여 배포하였다. 다른 이들에게 도움이 조금이라도 되면 보람이 될것 같다.

소스를 보면 플러그인을 추가하는 방법도 알수 있겠지만 쉽게 설명하는 글을 한번 더 정리해볼 예정이다.

Posted by 삼스
iOS2022. 6. 15. 10:24

SceneDelegate? 멀티윈도우?

AppDelegate로 관리되던게 어느순간 SceneDelegate란게 나타났다.
iOS13부터 등장한거 같은데...
SI특성상 iOS11부터 지원하는 경우가 많아서 아직까지 SceneDelegate를 사용하는 경우가 없긴 하나 알아두어야 하겠다.

AppDelegate는 iOS12와 이전버전에서 앱의 라이프사이클을 관리하는데 사용된다.
iOS13부터는 일부 역할을 SceneDelegate에 넘겨주게 되며 AppDelegate의 역할은 자연히 줄게 되었다.
AppDelegate는 여전히 앱이 시작되는 시작점을 비롯하여 앱레벨의 라이프사이클을 관리한다.

주요한 3개를 정리해 보면 
1. func application(_: didFinishLaunchingWithOptions:) -> Bool
앱 시작 포인트

2. func application(_: configurationForCOnnecting:options:)-> UISceneConfiguration
새로운 scene/window를 제공하려고 할때 호출됨. 최초에는 호출되지 않음

3. func application(_: didDiscardSceneSessions:)
scene이 종료될때 호출. 사용자가 multitasking이나 코드레벨에서 없애는 경우가 해당된다.


SceneDelegate

화면에 무엇을 보여줄것인지 처리한다.

1. scene(_: willConnectTo:options:)
scene은 하나의 UISceneSession을 갖게 되고 여기에서 처음 이 메서드가 호출된다.
contentView, window생성 그리고 window의 rootViewController를 설정한다.
storyboard를 사용하지 않고 개발하는 경우 여기서 UIWindow를 생성하고 rootViewController생성하는 코드를 직접 작성해야 한다.
storyboard를 사용한다면 특별히 작성해야 할 코드는 없다.

2. sceneWillEnterForeground(_:)
scene이 foreground로 전환될때 호출된다.

3. sceneDidBecomeActive(_:)
scene이 setup되고 화면에 표시될 준비가 완료된 상태에 호출된다.

4. sceneWillResignActive(_:)
active -> inactive 시 호출된다.

5. sceneDidEnterBackground(_:)
foreground -> background시 호출

6. sceneDidDisconnect(_:)
background상태에서 시스템이 자원을 확보하기 위해서 scene을 disconnect할 수 있다.

그렇다면 왜 이렇게 바꿨을까에 대한 생각을 안해볼수가 없지 않나?

이유는 iPad에서 Multiwindow를 지원하면서 나타났다.

멀티윈도우를 지원하려다 보니 AppDelegate가 하나의 UI를 관리하던 체제에서 여러 UI를 관리히야 하게 되었고 이 때 UI를 Scene단위로 구분하게 된것이다. 따라서 AppDelegate는 UI관련 처리를 하지 않게 되었고 이런 부분은 SceneDelegate에서 해야 하게 되었다.

멀티윈도우를 지원하지 않을거라면 궂이 SceneDelegate를 사용할 필요가 없다는 것이다.
SceneDelegate를 삭제하는 방법은 다음과 같다.

1. SceneDelegate.swift 삭제
2. UIWIndow를 AppDelegate로 이동
3. Info.plist에서 Application Scene Manifest삭제

AppDelegate는 UI관련 처리를 하지 않는 대신에 SceneSession이 추가되거나 삭제될때 보고를 받게 된다.

Scene은 안드로이드의 Activity가 시스템에 의하여 수거될수 있는것처럼 앱이 background상태일 때 수거될 수 있다.
이 때 해당 화면의 데이터를 보관했다가 나중에 Scene이 다시 새로 생성될 때 화면에 복원해주어야 할 수 있다.
이를 위해 func stateRestorationActivity(for scene: UIScene) -> NSUserActivity?를 제공하며 여기서 관심있는 데이터를 저장했다가 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 호출 시 저장했던 데이터로 화면을 복원할 수 있다.

SceneDelegate가 멀티윈도우를 지원하기 위해서 나왔고 따라서 ViewController가 하나만 생성되던 상황이 아닐수 있게 되었다. 이 때 고전적으로 ViewController에서 어떤 데이터가 생성되는 이벤트가 발생하면 그 내용을 화면에 바로 반영하는 방식으로 작업하게 되면 하나의 화면에만 그 내용이 반영되는 경우가 발생할 수 있다.
이를 해결하기 위해서는 데이터가 발생하면 모든 뷰컨트롤러에서 이를 처리하는 방식으로 수정되어야 한다.
3가지정도의 방법이 있다.

1. delegate로 이벤트 발생 시 모두처리
2. notification으로 이벤트 발생 시 모두 처리
3. Swift Combine Framework 사용

notification으로 처리하는것을 예를 들면 데이터가 발생하는 이벤트가 발생하면 바로 화면에 반영하지 않고 이벤트는 모델 컨트롤러에 저장하고 notification을 생성하여 전송한다. ViewController는 notification handler를 작성하고 여기서 모델컨트롤러의 데이터에 접근하여 화면에 반영한다. 그러면 모든 ViewController가 데이터의 변화를 감지하고 화면에 반영할수 있다.

이제 멀티윈도우이벤트를 발생시켜서 두개의 화면으로 나뉘게 하는 방법을 알아볼 차례인데...
아직 이런걸 만들어 볼일이 없어서 나중에 필요가 생기면 알아보기로 하겠다.

Posted by 삼스
iOS2022. 3. 29. 10:37

스토어 계정 만료 시
- 앱다운로드 불가
- 새로운 앱과 업데이트 게시 불가
- 사용중이던 앱의 사용은 문제 없음
스토어계정 멤버쉽 갱싱 시
- 무료앱은 24시간내에 스토어에 다시 게시 되어 검색이 가능해짐
- 유료앱은 AppStoreConnect에서 "유료 응용프로그램 계약서"를 작성하면 사용가능
- 재배포 할필요 없음.

엔터프라이즈 계정 만료시
- 앱다운로드 불가
- 이미 설치된 앱도 사용불가

엔터프라이즈 인증서 만료 시 
- OCSP Server를 통해 인증을 진행하는데 캐시기간(일주일정도)동안은 
사용이 가능할 수 있으나 이 기간이 지나면 앱 사용이 불가해짐.
- 인증서는 3년간 유효 하나 "프로비저닝 프로파일"이 1년주기로 만료되기 때문에 매년 배포해야 함
- 인증서로 프로파일을 만들고 앱에 포함 때문에 프로파일이 만료되기 전에 만료되지 않은 인증서로 
프로파일을 갱신하여 배포해야 함.

Posted by 삼스
iOS2021. 6. 11. 14:33

앞서 안드로이드에서 Coroutine과 CompletableFuture를 알아 보았다.

제목에서 보면 알수 있듯이 개발자들은 지속적으로 게을러지기 위해 노력하고 있다는것을 알 수 있다.

비동기 호출도 가장 적은 타이핑을 목표로 하고 있다.

 

비동기 처리를 하는 가장 고전적인 방법은 콜백함수를 인자로 하여 처리과 완료되면 콜백으로 처리결과를 전달하여 다음 단계로 넘어가도록 하는 방법이 되겠다.

알다시피 이는 콜백지옥을 수반하게 되며 이게 싫었던 개발자는 Rx나 CompletableFuture같은것들을 고안하고 사용하기 시작한다.

 

그것도 싫었던 그들은 Coroutine같은것을 다시한번 고안해 냈으며 마치 동기호출을 하는 코드처럼 비동기코드를 짜기 시작했다.

 

안드로이드에서는 Coutine으로 아래와 같이 마치 동기호출처럼 비동기 함수들을 호출할 수 있다.

suspend iWantEatRamyeon(money) {
  try {
    val ramyeon = buyRamyeon(money)
    val pot = getPot()
    val cookedRamyeon = doBoilPot(ramyeon, pot)
    val ramyeonInDish = moveToDish(cookedRamyeon)
    eatIt(ramyeonInDish)
  } catch (e: Exception) {
    // 실패케이스 처리
  }
}

 

 

iOS에서는 swift 5에서 아래와 같이 사용 가능하다.

func iWantEatRamyoen(money) async {
    val ramyeon = await buyRamyeon(money: money)
    val pot = await getPot()
    val cookedRamyeon = await doBoilPot(ramyeon:ramyeon, pot: pot)
    val ramyeonInDish = await moveToDish(ramyeon: cookieRamyeon)
    eat(ramyeon: remyeonInDish)
    print("Server response: \(response)")
}

 

async 메서드에서 비동기메서드로 호출하려면 메서드 정의시 아래와 같이 정의해야 한다.

 

func buyRamyeon(money: money) async -> Ramyeon {
	// get a ramyeon 
	var ramyeon = getRamyeon()
	return ramyeon
}

 

DispatchQueue와 블럭코딩을 이용하여 위와 동일한 로직을 작성해보자.

그러면 위 코드가 얼마나 코딩량을 줄여주는지 알게 될것이다.

 

개발자는 게을러져야 하나보다... 머리는 빼고...

Posted by 삼스
iOS2020. 7. 28. 13:54

앱익스텐션을 개발할 준비가 되었다면 먼저 적절한 익스텐션포인트를 결정해야 한다. 원하는 익스텐션포인트에 해당하는 Xcode 익스텐션 템플릿으로 시작하여 코드와 UI를 커스텀하는 방식으로 진행하는것이 좋다. 테스트와 최적화가 완료된 후 포함하는 앱에 함께 배포할 준비가 된것이다.

 

적절한 익스텐션포인트를 선택하여 개발 시작하기

 

익스텐션포인트가 잘 정의되어 있기 때문에 이를 선택하는것이 우선이다. 이를 선택함으로써 사용가능한 API와 동작이 결정된다.

 

익스텐션포인트는 iOS, OS X에서 제공되고 Info.plist에 익스텐션포인트 아이디키로 정의되어 있으며 다음 링크에서 확인이 가능하다.

 

익스텐션포인트를 선택하면 새로운 타겟을 추가한다. 가장 쉬운 방법은 기본설정을 모두 가지고 있는 Xcode template을 사용하여 추가하는 방법이다.

 

Xcode프로젝트에 타겟을 추가하려면 File > New > Target를 선택. Application Extension for iOS 또는 macOS 를 선택. 그러면 다음 그림에서처럼 선택가능한 템플릿들이 나온다. 

 

 

 

템플릿을 선택하면 익스텐션코드를 수정하기 전에 빌드하고 실행해볼수 있다. 빌드가 완료되면 .appex로 끝나는 익스텐션번들을 얻을 수 있다.

 

대부분 디폴튼 익스텐션을 시스템프리퍼런스나 셋팅에서 활성화함으로써 다른앱을 통해서 접근하여 테스트가 가능하다. 예를 들어 OS X extension을 사파리에서 웹페이지를 오픈하고 툴바의 공유버튼을 선택할 수 있고 이 때 나타나는 당신의 익스텐션을 선택 가능하다.

 

디폴트익스텐션 템플릿 살펴보기

 

각 익스텐션템플릿은 Info.plist, 하나의 view controller클래스 그리고 기본 UI를 포함하고 있으며 이것들은 익스텐션포인트에서 정의된 것들이다. 기본 view controller 클래스(또는 기반클래스)는 반드시 구현해야 하는 필수 메서드들을 가질 수 있다.

 

앱익스텐션포인트 타겟의 Info.plist에 익스텐션에 대한 상세한 정보를 포함할 수 있다. NSExtension키가 기본으로 있으며 Dictionary로 키/값쌍으로 익스텐션포인트를 기술한다. 예를 들어 NSExtensionPointIdentifier 키는 익스텐션포인트의 리버스 DNS명이다. 다음은 NSExtension 딕셔너리에서 볼 수 있는 키/값들이다.

 

- NSExtensionAttributes 익스텐션포인트에 해당하는 속성들의 딕셔너리로 PhotoEditing익스텐션의 경우 PHSupportedMediaTypes가 예이다.

- NSExtensionPrincipalClass 템플릿에서 생성하는 기본뷰컨트롤러 클래스의 이름으로 SharingViewController같은 호스트앱이 익스텐션을 호출할때 이 클래스를 인스턴스화하게 된다.

- NSExtensionMainStoryboard(iOS 익스텐션만 해당)  기본 스토리보드파일, MainInterface로 보통 명명됨

 

Info.plist외에 일부 capability가 추가될 수 있다. iOS Document Provider익스텐션의 경우 com.apple.security.application-groups 인타이틀먼트를 포함한다.

 

모든 OS X 앱익스텐션 템플릿들은 App Sandbax와 com.apple.security.files.user-selected.read-only 인타이틀먼트를 기본으로 포함한다. 익스텐션이 네트웍이나 연락처의 사진에 접근한다면 추가적인 capability  를 포함해야 한다.

 

 

 호스트앱의 요청에 응답하기.

 

호스트앱에서 유저가 익스텐션을 선택하면 익스텐션이 오픈되고 호스트앱의 요청이 발생한다. 고수준에서는 익스텐션은 이 요청을 받게되며 유저의 작업을 수행하고 사용자의 액션에 따라 요청을 완료하거나 취소한다. 

공유익스텐션의 경우 호스트앱의 요청을 받아서 표시된 뷰에서 응답한다. 유저가 내용을 구성하고 포스트 전송 또는 취소를 선택하면 익스텐션은 완료 또는 취소를 처리한다.

 

호스트앱이 요청을 전달하면 거기에는 익스텐션의 컨텍스트를 기술한다. 많은 익스텐션에서 익스텐션 컨텍스트에서 중요한 부분은 익스텐션에서 원하는 작업에 대한 아이템을 기술하는 것이다. 예를 들어 OS X 공유 익스텐션의 컨텍스트는 유저가 포스팅하는데 필요한 선택된 테스트가 될것이다.

 

호스트앱의 요청이 발생하면 앱익스텐션은 기반 뷰컨트롤러의 extensionContext프로퍼티를 컨텍스트를 얻기 위해 사용할 수 있다. 자식뷰컨트롤러 역시 체이닝을 통해 이 프로퍼티에 접근 할 수 있다.

 

이어서 NSExtensionContext클래스로 컨텍스트의 아이템을 사용할 수 있다.  뷰컨트롤러의 loadView메서드에서 컨텍스트와 아이템에 접근할 수 있으며 이 정보를 뷰에 표시할 수 있다. 다음과 같이 이 작업을 수행할 수 있다.

 

  1. NSExtensionContext *myExtensionContext = self.extensionContext;

컨텍스트 객체의 inputItems에 익스텐션에서 필요한 아이템들이 있다. NSExtensionItem객체의 배열을 답고 있으며 다음과 같이 사용 가능하다.

 

  1. NSArray *inputItems = myExtensionContext.inputItems;

각 NSExtensionItem객체는 아이테믜 유형을 설명하는 여러개의 프로퍼티(title, content text, attachment, userInfo등)를 담고 있다. 

 

attachments 프로퍼티는 아이템과 관련된 미디어데이터 배열을 포함한다. 공유요처어의 경우 유저가 공유하고자 하는 웹페이지의 표현이 포함될수 있다.

 

input items 으로 작업 후 보통 유저에게 테스크를 완료 혹은 취소를 선택하도록 한다. 유저의 선택에 따라 completeRequestReturningItems:completionHandler: 메서드를 호출하는데 NSExtensionItem객체를 추가로 반환할 수 있고 cancelRequestWithError:메서드로 에러코드를 반환할 수 있다.

 

iOS에서 앱익스텐션이 잠재적으로 긴 작업을 완료하기 위해 긴 시간이 필요할 수 있다. 웹사이트에 컨텐츠를 업로드할 때 가 해당된다. 이럴경우 NSURLSession클래스로 백그라운드에서 전송을 시작할 수 있다. 백그라운드 전송이 ㅂ ㅕㄹ도의 프로세스를 사용하기 때문에 전송은 계속될 수 있고 낮은 우선순위의 테스크로 처리되고 익스텐션이 완료된 후 종료된다.

 

업로드/다운로드 이외의 VoIP, 오디오 백그라운드 재생같은 백그라운드 작업은 불가하다. 만일 UIBackgroundModes키를 익스텐션의 Info.plist에 기술한다면 AppStore에서 리젝될것이다.

 

효율성 및 성능 최적화

 

앱익스텐션은 유저에게 빠르고 가벼운 느낌을 주어야 한다. 1초이내의 빠르게 적재되도록 앱익스텐션을 설계해야 한다.

 

포그라운드앱의 메모리제한보다 앱익스텐션은 훨씬 더 적게 사용해야 한다. 두 플랫폼에서 모두 시스템은 유저가 호스트엡의 주목적에 맞게 돌아가기를 원하기 때문에 익스텐션을 적극적으로 종료할것이다. 일부 익스텐션은 더 적은 메모리를 사용한다. 예로 위젯은 동시에 유저가 몇개의 위젯을 열수도 있기 때문에 더 효율적이어야 한다.

 

앱익스텐션은 자신의 메인 루프를 가지고 있지 않기 때문에 메인루프에서 적절한 동작을 하려면 설정된 룰을 따라야 한다. 예로 익스텐션이 메인루프를 블럭한다면 이는 다른 익스텐션이나 앱에세 좋지않은 사용자경헙을 선사하게 될것이다.

 

GPU는 시스템에서 리소스를 공유한다는 것을 상기해야 한다. 공유리소스에 대해 앱익스텐션은 우선순위가 낮다. 예로 투데이 위젯이 그래픽이 강조된 게임에서 사용된다면 아주 나쁜 결과를 초래할것이다. 메모리가 부족해지면 시스템은 이런 익스텐션을 죽이려고 한다.  시스템리소스를 많이 사용하는 기능은 앱이 적합하며 앱익스텐션은 그렇지 않다.

 

간소화된 UI설계

 

대부분의 앱익스텐션은 당신의 익스텐션을 오픈할때 보게되는 커스텀UI를 제공해야 한다. UI는 단순하고 억제되고 단일작업을 촉진하는데 집중해야 한다. 성능과 UX를 개선하고자 한다면 익스텐션이 제공하는 기능을 지원하지 않는 추가적인 UI는 피해야 한다.

 

Xcode 앱익스텐션 템플릿은 시작하는데 필요한 UI를 제공한다.

 

아이콘은 일관성과 가독성을 위해 포함하는 앱과 동일한게 좋다. 

 

액션익스텐션은 포함하는 앱의 아이콘의 이미지버전을 사용한다. 반드시 제공되어야 한다.

 

공유익스텐션은 자동으로 포함하는 앱의아이콘을 갖는다. 공유익스텐션 타겟에 다른 아이콘을 제공해도 Xcode는 이를 무시한다. 

나머지 다른 익스텐션들도 앱의 아이콘을 제공해야 한다.

 

앱익스텐션 이름은 짧고 가독성좋은 이름이어야 하며 포함하는 앱의 이름을 포함해야 한다. 패턴은 <포함하는앱이름>-<앱익스텐션이름>이다. 이는 유저가 시스템에서 쉽게 다룰수 있게 해준다. 옵션으로 포함하는앱이름을 사용해도 되는 경우가 있는데 이 경우는 오직 하나의 익스텐션만 제공하는 앱의 경우이다.

 

앱익스텐션이름은 타겟의 CFBundleDisplayName값이다. 이 값을 지정하지 않으면 CFBundleName값이 사용되며 포함하는 앱이름이 된다.

 

로컬화된 앱익스텐션으로 다국어가 적용된 앱익스텐션이름을 제공할 수 있다.

 

일부 앱익스텐션은 짧은 설명글이 필요할 수 있는데 OS X 위젯의 경우 투데이 뷰에서 보기를 원하는 위젯을 선택하는데 도움을 준다. InfoPlist.strings 파일에서 widget.description키의 값을 편집하면 된다.

 

모든 디바이스에서 iOS 앱익스텐션 지원하기

 

universal을 반드시 지원해야 한다. 아이폰, 아이팟터치, 아이패드등에서 모두 가능해야 한다. 포함하는 앱이 어떤 디바이스를 타겟으로 하는것과는 무관하다. Xcode 앱익스텐션 템플릿은 유니버셜로 설정되어 있다.

 

Xcode 빌드설정에서 디바이스패밀리를 "Universal"로 지정해야 한다.

 

 

AutoLayout과 size클래스를 UI 디자인시 적용한다. 그러면 모든 디바이스 사이즈와 방향에 대응될것이다. 

 

 

디버그, 프로파일링 그리고 테스트

 

앱익스텐션디버그는 다른 프로세스 디버깅하는 방법과 한가지만 제외하고 아주 비슷하다. 앱익스텐션의 Run phase에서 실행파일로 호스트앱을 지정한다. 지정한 호스트앱의 UI로 앱익스텐션에 접근하면 Xcode 디버거가 익스텐션에 연결한다.

 

Xcode 앱익스텐션 템플릿의 scheme에서 Ask On Launch 옵션이 사용된다. 이 옵션을 사용하면 앱을 빌드하여 실행할 때 마다 어떤 호스트앱을 사용할지 지정할 수 있다. 이렇게 하지 않고 개별적으로 호스트를 지정하고 싶다면 scheme editor에서 info tab의 app extension scheme's Run phase를 이용한다.

 

다음은 앱익스텐션에 Xcode debugger를 연계하는 절차이다.

 

1. Produce > Scheme > MyExtensionName 또는 toobar의 MyExtensionName 의 scheme 팝업메뉴 선택

2. 지정한 호스트앱을 시작하기 위해 Build and Run 버튼 선택

3. 호스트앱의 UI에서 앱익스텐션 호출하면 Xcode 디버거가 앱익스텐션의 프로세스에 연결되고 브레이크포인트가 활성화되고 익스텐션이 시작된다. 그러면 다른 프로세스의 디버깅할떄와 동일하게 디버깅 가능하다.

 

디버깅전에 익스텐션 scheme이 선택되어 있는지 확인해야. 앱의 scheme을 선택하면 앱익스텐션 디버깅이 아니고 앱디버깅이다.

 

OS X에서 호스트앱 테스트 및 디버깅으로 접근하기 전에 앱익스텐션활성화 단계를 수행해야 한다. 시스템 프리퍼런스의 익스텐션 페인에서 대부분의 익스텐션타입을 활성화 하라. 공유나 액션메뉴의 More를 선택하여 익스텐션 페인을 오픈할수도 있다.

 

OS X 투데이위젯을 위해 위젯시뮬레이터로 테스트 및 디버깅할 수 있다.

 

iOS에서 커스텀 키보드를 위해 Settings > General > Keyboard > Keyboards에서 활성화해야 한다.

 

OS X 디버깅 세션 동안에 Xcode는 빌드된 앱익스텐션을 등록한다. 이는 개발버전을 OS X에 설치하려면 FInder로 빌드위치에서 애플리케이션폴더같은 위치로 복사해야 한다. 

 

Xcode 디버깅콘솔로그에서 앱익스텐션 바이너리는 CFBundleDisplayName프로퍼티값 대신에 CFBundleIdentifier프로퍼티값이 사용된다.

 

앱익스텐션이 효율적이고 응답성이 있어야 하기 때문에 앱익스텐션 동작중에 디버그네비게이터의 디버그게이지를 살피는것이 좋다. 디버그 게이지는 CPU, 메모리, 다른 시스템리소스들의 사용량을 볼수 있게 해준다. 성능이슈의 증거를 보게 된다면 앱익스텐션을 프로파일하기 위해 instrument를 사용할 수 있고 향상된 부분을 확인할수도 있다. 

 

앱배포

 

포함하는 앱없이 스토어에 앱익스텐션을 배포할수 없으며 다른앱으로 이전할수도 없다.

 

iOS 앱익스텐션을 배포하려면 포함하는 앱을 스토어에 배포해야 한다.

OS X 앱익스텐션을 배포하려면 스토어에 포함하는 앱을 배포하는것이 추천되나 필수는 아니다.

 

Mac App Store가 아닌 다른 채널로 OSX 앱익스텐션을 배포하면 Getekeeper가 유저가 오픈할 때 걸러내게 되며 포함하는 앱을 승인한다. 또한 개발자 ID외의 인증서로 서명된 경우 사용자는 Gatekeeper를 명시적으로 재정의하여 익스텐션을 사용할수 있도록 포함앱을 열어야 한다.

 

리뷰를 통과하려면 포함하는앱은 유저에게 기능을 제공해야 한다. 앱익스텐션만 제공할수는 없다.

 

Posted by 삼스
iOS2020. 7. 28. 13:54

앱익스텐션은 앱이 아니며 제한되고 잘 범위지어진 정책으로 정의된 각각 익스텐션포인트에 맡게 구현된다.

 

앱익스텐션 생명주기

 

앱이 아니기 때문에 생명주기나 환경이 앱과 다르다. 대개는 뷰컨트롤러에서 표시된 앱의 UI로부터 유저에게 선택되어졌을때 시작된다. 앱익스텐션을 선택하여 실행해주는 앱을 호스트앱이라고 칭한다. 호스트앱은 앱에 제공되는 컨텍스트를 제공하며 유저의 액션의 응답으로써 리퀘스트가 전달될 때 익스텐션의 생명주기가 시작된다. 그리고 호스트앱으로부터 전달받은 요청을 모두 처리하게 되면 익스텐션도 종료된다.

 

예를 들어 OS X 호스트앱에서 텍스트일부를 선택했다고 상상해보자, 공유버튼이 활성화 되고 텍스트를 공유할 수 있는 공유목록에서 익스텐션하나를 선택한다. 호스트앱은 유저의 선택에 응답으로 선택한 테스트를 포함하는 요청을 앱익스텐션에 전달하게 된다. 이 상황에 대한 일반화한 다음 그림을 참고하라.

 

 

 

그림의 step2에서 시스템은 호스트앱의 요청을 포함하는 앱익스텐션의 인스턴스를 만들고 서로간의 통신채널을 셋업한다. 익스텐션은 호스트앱의 컨텍스트내에서 표시되어지고 작업을 수행하기 위한 호스트앱 요청을 수신한다(여기서는 선택한 텍스트이다).

 

그림의 step3에서 유저는 수행하거나 취소할 수 있고 닫을수도 있다. 이 액션의 응답으로 익스텐션은 유저의 작업을 바로 수행함으로 써 호스트앱의 요청을 완료하거나 필요하다면 백그라운드 프로세스로 수행한다. 호스트앱은 익스텐션뷰를 떼어내게 되고 유저는 이전 컨텍스트로 돌아가게 된다. 익스텐션의 작업이 끝나면 바로 또는 후에 결과가 호스트앱에 반환될것이다.

 

step4처럼 앱익스텐션의 작업을 수행한 후 시스템은 익스텐션을 종료한다.

 

 

앱익스텐션 통신

 

앱익스텐션은 기본적으로 호스트앱과 통신한다. 그리고 트랜잭션프로세싱처럼 동작한다. 호스트앱에서 요청을 보내고 앱익스텐션에서 응답을 받는다.다음 그림에서 호스트앱에서 시작된 실행중인 익스텐션과 이를 포함하는 앱간의 관계를 보여준다.

 

 

 

앱익스텐션과 이를 포함하는 앱간에 직접적인 통신은 없다 포함하는 앱은 포함된 익스텐션이 수행중에 실행되지 않는다. 포함하는 앱은 호스트앱과 통신하지 않는다.

 

일반적인 요청/응답의 트랜잭션처럼 시스테메서 호스트앱대신에 앱익스텐션을 오픈하며 호스트에서 제공하는 익스텐션컨텍스의 데이터를 전달한다. 익스텐션은 UI를 표시하고 작업을 수행한다. 그리고 익스텐션의 목적에 부합하는 데이터를 호스트에 반환한다.

 

그림에서 점선은 익스텐션과 포함하는 앱간에 유효한 상호작용이 제한된다는것을 의미한다. 투데이위젯은 시스템에 포함하는 앱의 오픈을 요청할 수 있는데 NSExtensionContext 클래스의 openURL:completionHandler: 메서드로 가능하다.

 

아래 그림에서 Read/Write 화살표를 보면 앱익스텐션과 포함하는 앱은 private으로 정의된 shared container에 공유된 데이터에 접근할 수 있다.

 

 

 

앱익스텐션에서 할수 없는 것들

 

시스템에 그 책임이 집중되기 때문에 앱익스텐션은 일부 작업은 불가하다. 아래 나열한것들은 수행 불가하다.

 

- sharedApplication 객체에 접근 불가하여 이 객체의 모든 메서드사용불가

- NS_EXTENSION_UNAVAILABLE 메크로, 잘알고 있는 메크로 또는 유효하지 않는 프레임워크등의 사용 불가, 예) HealthKit, EventKit UI framework등

- 카메라, 마이크 사용불가

- 오랫동안 동작하는 백그라운드테스크 사용불가. (NSURLSession을 이용한 upload/download를 일으킬수 있고 포함하는 앱에 결과를 보고할 수 있다)

- AirDrop으로 데이터 수신할수 없음(UIActivityViewController로 데이터를 보내는것은 가능)

Posted by 삼스
iOS2020. 7. 28. 13:53

App Extension으로 영향력을 증대하기

 

앱익스텐션은 앱을 뛰어 넘어 컨텐츠와 기능을 더 확장할 수 있게 한다. 

임의작업을 활설화하기 위해 앱익스텐션을 작성할 수 있다. 예를 들어 브라우저에서 당신의 소셜서비스에 유저가 포스팅하도록 공유 익스텐션을 제공할 수 있다. 또는 유저가 좋아하는 팀을 따라잡기 위해 알림센터에서 현재 점수를 표시하는 투데이 위젯을 제공할 수 있다. 시스템 키보드를 대체하는 커스텀 키보드를 제공할 수도 있다.

 

iOS와 macOS에서 몇가지 타입의 앱익스텐션을 정의한다. 

앱익스텐션이 활성화된 하나의 시스템영역은 하나의 익스텐션포인트로 부른다. 

익스텐션포인트는 사용정책과 해당 영역에서 앱익스텐션을 생성할 때 사용하는 API들을 제공한다.

 

다음 표는 iOS와 macOS에서 제공하는 앱익스텐션의 목록을 제공하며 각 익스텐션의 익스텐션포인트를 예로 들고 있다.

 

Action(iOS, macOS; UI, nonUI)

- host앱에서 컨텐츠를 다루거나 표시한다.

Audio Unit(iOS, macOS; UI, nonUI)

- host앱에 전달할 오디오스트림을 생성하거나 host앱으로부터 오디오스트림을 받아서 수정 한 후 host앱으로 다시 전달한다.

Broadcast UI(iOS, tvOS)

Broadcast Upload(iOS, tvOS)

Call Directory(iOS)

- 전화번호를 구별하고 블럭한다. CallKit Framework로 구현 가능하다.

Content Blocker(iOS, macOS)

- 컨텐츠블로킹앱이 규칙을 업데이트 했는지 표시(UI없음)

Custom Keyboard(iOS)

- 시스템키보드를 대체함.

Document Provider(iOS; ui, nonUI)

- 파일저장소에 접근하고 조작한다.

Finder Sync(macOS)

- Finder에 파일 동기화 상태를 바로 반영(Ui없음)

Game App(watchOS)

- Apple Watch에서 게임앱을 작성할 수 있게 해줌.

iMessage(iOS)

- 메세지앱과 상호작용

Intents(iOS)

- Siri와 통합하여 작업을 처리할 수 있다. SiriKit Programming Guide참조

Notification Content(iOS)

Notification Service(iOS)

Photo Editing(iOS, macOS)

- 사진앱으로 사진이나 비디오를 편집

Share(iOS, macOS)

- wetsite나 컨텐츠들을 공유하는 포스팅

Smart Card Token(macOS)

Spotlight Index(iOS)

- 앱 동작 중이 아닌동안에 앱의 컨텐츠를 인덱스함

Sticker Pack(iOS)

- 사용자가 메세지앱에서 사용할 수 있는 스티커셋을 제공

Today(iOS, macOS)

- 알림센터의 투데이에 빠르게 업데이트하거나 빠르게 태스크를 수행

TV Services(tvOS)

VPN(iOS, macOS)

- VPN client

WatchKit App(watchOS)

- 애플워치를 위한 알림UI나 앱을 제공

Xcode Source Editor(macOS)

 

시스템에서 각 앱익스텐션에 대한 제한된 영역을 정의하기 때문에 작성하려고 하는 앱에 가장 적합한 영역을 선택하는것이 중요하다. 경험공유를 활성하한 앱을 작성하고자 한다면 Share Extension Xcode template로 시작하여 공유익스텐션포인트를 사용해야 한다.

 

앱익스텐션과 앱은 구별된다.

 

앱에 익스텐션을 추가하고 배포한다고 해도 앱익스텐션은 바이너리가 분리되고 앱과 독립적으로 동작한다.

 

앱익스텐션은 새로운 타겟으로 생성하게 된다. 다른 타겟들 처럼 익스텐션타겟도 각각 셋팅과 빌드에 포함되는 파일들을 지정하게 된다. 하나의 앱에 여러개의 익스텐션타겟을 추가할 수 있다.

 

익스텐션개발을 시작하는 가장 좋은 방법은 xcode가 제공하는 템플릿으로 시작하는 것이다. 각 템플릿들은 익스텐션포인트에 맞는 구현파일과 셋팅을 포함한다.

 

유저에 앱익스텐션을 배포하려면 앱스토어에 앱과함께 제출하면 된다. 유저가 앱설치 시 익스텐션도 함께 설치된다.

 

앱익스텐션을 설치한 후 유저는 반드시 활성화하는 액션을 수행해야 한다. 유저는 유형에 따라 익스텐션을 활성화할 수 있다. 예를 들어 투데이위젯 익스텐션인 경우 유저는 익스텐션을 활성화 하기 위해 알림센터의 투데이뷰를 편집할 수 있다. 혹은 어떤 경우는 iOS는 설정에서 macOS는 시스템프리퍼런스에서 활성화 할 수 있다.

 

각 앱익스텐션 타입별로 서로 다른 작업유형을 활성화함에도 불구하고 대부분의 익스텐션에 공통적인 UX 부분이 있다. 가장 훌룡한 UX는 빠르고 이어지고 하나의 작업에 집중하는 것이다.

 

유저는 시스템에서 제공되는 일련의 UI에 반을하여 당신의 앱익스텐션을 오픈하게 된다. 예를 들어 공유익스텐션은 시스템에서제공되는 공유버튼에 의해 표시된 목록에서 당신의 앱익스텐션을 선택함으로써 비로소 활성화된다.

 

대부분의 앱익스텐션이 자체 커스텀 UI요소를 갖고 있지만 앱익스텐션에 진입하기 전까지 유저는 커스텀UI를 보지 않는다. 유저가 당신의 앱익스텐션에 진입할 때 당신의 커스텀 UI는 사용자에세 새로운 맥락으로 이동되었다는것을 알게 한다. 현재 사용중인 앱과 당신의 익스텐션을 구분할 수 있기 때문에 오동작하거나 제대로 수행하지 못하는 액스텐션을 삭제할 수 있게 해준다.

 

 

 

Posted by 삼스
iOS2020. 7. 10. 17:23

class Log {
static func debug(_ msg: String, file: String, line: Int) {
print("\(Date()) \(file.components(separatedBy: "/").last ?? file) line \(line) \(msg)")
}
}

사용예...
Log.debug("log message", file:#file, line:#line)



Posted by 삼스