2018/2/7의 WebKit의 포스팅에 의하면 Safari와 SFSafariViewController
에서 Service Worker API를 지원한다고 합니다.
11.3 부터 지원이 된 것 같은데 이 시점에는 WKWebView
에서 지원이 된 것은 아니었던 것으로..
Service Worker
는 (가능한 경우) 기본적으로 웹 애플리케이션, 브라우저 및 네트워크 사이에 있는 프록시 서버 역할을 한다. 이는 그 무엇보다도 효과적인 오프라인 경험의 생성을 가능하게 하고, 네트워크 요청을 가로채서 네트워크가 이용 가능 여부에 따라 적절한 조치를 취하고, 서버에 있는 Asset을 업데이트하기 위한 것이다. 또한 푸시 알림과 백그라운드 동기화 API에 대한 액세스를 허용한다.
개인적으로 이해하기로는, JS 베이스로 브라우저와는 별도로 추가로 동작하는 일꾼을 하나 둡니다. 이 일꾼을 통해 초기에 로딩했던 컨텐츠들을 관리하고, 새로운 업데이트가 필요하다면 서버에 요청하고, 그렇지 않을 경우 기존 리소스들을 사용하여 오프라인 상황에서도 정상적으로 동작하도록 효율적인 웹앱을 만들 수 있습니다. Service Worker를 이야기 하다 보면 PWA 개념이 나오는데, 연결된 링크를 확인해주세요.
아무튼 이 Service Worker API를 iOS 13 이후에서는 WKWebView
에서 지원한다고 하는데, 별도의 API가 지원되는 것은 아닌 것 같고, navigation.serviceWorker
에 접근해서 사용하는 듯 합니다.
결론은, .entitlements
파일에 com.apple.developer.WebKit.ServiceWorkers
를 true
로 지정하니까 되었습니다.
테스트 방법은 다음과 같습니다.
navigation.serviceWorker
가 실존하는지 체크하기사실 저 entitlements key가, 공식 문서 상으로는 없는 것 같은데 다들 어떻게 알아오시는 걸까요 대단.. 연관된 가장 최신 문서를 따르자면 com.apple.developer.web-browser
를 사용하여 기본 브라우저 허용을 받으면 Service Worker 를 WKWebView에서 사용할 수 있다고 합니다.
기본 브라우저로 설정되면서 자연스럽게 serviceWorker 항목이 true 로 지정되는 것일까용.
안녕하세요! caution입니다.
우리가 무심코 사용했던 true && false
에서 사용되는 &&
에 대해 알아보려고 합니다.
이 함수가 어떻게 생겼는지 아시나요? 요렇게 생겼숩니다.
/// - Parameters:
/// - lhs: The left-hand side of the operation.
/// - rhs: The right-hand side of the operation.
public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool
여기서 autoclosure
라는 키워드가 나오는데 뭔지 아시나요? 저는 몰라서 >< 찾아보기로 하였습니다.
autoclosure
는 함수에 인수로 전달되는 표현식을 자동적으로 감싸주는 closure
를 말합니다. 이것은 어떤 인수도 필요하지 않고, 이 closure
불렸을 때, 그것은 그 안에 있는 표현식의 값을 반환합니다. 이러한 구문상의 편의성은 함수의 파라미터로 명시적 closure
를 사용하는 대신 주변의 중괄호를 생략하면서 일반적인 표현식을 작성할 수 있도록 합니다.
autoclosure
를 흔히 호출하지만 이러한 기능을 구현하는 것은 일반적이지 않습니다. 예를 들어, assert(condition:message:file:line:)
함수는 condition
및 message
매개 변수 autoclosure
로 받습니다. condition
인수는 디버그 빌드에서만 수행되고 조건이 거짓인 경우에만 메시지 message
인수가 수행됩니다.
autoclosure
를 사용하면 closure
를 호출할 때까지 내부 코드가 실행되지 않도록 지연시킬 수 있습니다. 이러한 Delaying evaluation
은 코드 수행 시기를 제어할 수 있기 때문에 side-effect
가 있거나 계산 비용이 많이 드는 코드에 유용합니다. 아래 코드는 closure
가 수행을 지연시키는 방식을 보여줍니다.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
customersInLine
의 첫 번째 요소는 closure
내부의 코드에 의해 제거되며, 배열의 요소는 closure
가 실제로 호출될 때까지 제거되지 않습니다. closure
를 호출하지 않으면 closure
내부의 식이 평가되지 않으며, 이는 배열 요소가 제거되지 않음을 의미합니다. customerProvider
의 type은 String
이 아니라 () -> String
으로, 문자열을 반환하는 매개 변수가 없는 함수입니다.
함수에 매개 변수로 closure
를 전달할 때에도 동일한 방식으로 closure
수행을 지연시킬 수 있습니다.
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
위의 serve(고객:)
함수는 고객의 이름을 반환하는 명시적 closure
를 인수로 받습니다. 아래 버전의 serve(고객:)
동일한 작업을 수행하지만 해당 매개 변수의 유형을 명시적 closure
가 아닌 @autoclosure
특성으로 표기하여 autoclosure
를 받습니다. 이제 매개 변수로 closure
대신 문자열을 사용한 것처럼 함수를 호출할 수 있습니다. customerProvider
매개 변수의 유형이 @autoclosure
특성을 가지므로 인수가 자동으로 closure
로 변환됩니다.
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
scope 탈출 허용되는 autoclosure
를 원하는 경우 @autoclosure
및 @escaping
특성을 모두 사용하십시오. @escaping
특성은 위의 Escaping Closures를 참고하세요.
// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
closure
autoclosure
위의 코드에서 collectCustomerProviders(_:)
기능은 전달된 customerProvider
인수 closure
를 호출하는 대신 collectCustomerProviders
에 closure
를 추가합니다. 배열은 함수의 범위 밖에 선언됩니다. 즉, 함수가 반환된 후 배열의 closure
를 실행할 수 있습니다. 따라서 customerProvider
매개 변수의 값이 함수의 범위를 벗어날 수 있도록 허용되어야 합니다.
해당 intent들을 처리하는 커스텀 객체에 intent를 전달하기 위한 인터페이스
protocol INIntentHandlerProviding
INIntentHandlerProviding
프로토콜은 SiriKit 요청을 Intents Extension의 적절한 객체로 라우팅하는 방법을 정의합니다. 이 프로토콜의 메소드는 특정 Intent
객체를 해결/확인/처리가 가능한 객체를 반환하는 디스패처 역할을 합니다. 이 프로토콜은 INExtension 클래스가 채택하고 있으며 사용자에 맞게 커스텀하여 제공해야 합니다.
프로젝트에 Intent
extension을 추가하면, Xcode는 자동으로 이 프로토콜을 채택한 기본 INExtension
하위클래스를 만듭니다. 이 하위클래스의 hadler(for:)
메서드를 수정하여 처리 객체를 생서하도록 사용하세요. 여러 Intent
에 대해 같은 클래스를 사용하거나, 각 Intent
에 따라 다른 클래스를 사용해도 됩니다.
안녕하세요! caution입니다.
SiriKit을 써보고 싶었던 건 아니고, 단축어를 앱에 적용해보고 싶었는데 그게 SiriKit 내부에 포함되나 보더라고요 (맞나) 그래서 급작스런 SiriKit 입니다.
앱 서비를 위해 사용자의 요구사항을 resolve(해결하고), confirm(확정해주고), handle(다루기)
SiriKit은 Intents app extension
과 함께 제공된 정보를 기반으로 사용자의 요청을 수행할 수 있는지 확인합니다. 해결 단계에서 intent
객체의 개별 매개 변수(parameter)를 확인하고 필요에 따라 SiriKit에 설명을 요청합니다. 확인 단계에서 모든 intent
파라미터에 대한 마지막 점검을 한 번 수행하고, 서비스가 intent
의 요구를 이행할 준비가 되었는지 확인합니다. intent
가 유효하다면, SiriKit에서 해당 의도를 당신이(우리 앱이) 처리하도록 요청합니다.
중요
watchOS는 모든 의도를 지원하지 않습니다. 예를 들어, watchOS의
Intents App Extension
은 비디오 통화를 시작하거나 메시지 속성을 설정하거나 CarPlay 도메인 관련intent
를 처리할 수 없습니다. watchOS에서 이들을 처리하기 전에 가능 여부를 먼저 확인하세요.
그럼 이만! 뿅!
안녕하세요! caution입니다.
Swift 5.0 부터 unowned 가 Optional Type이 가능해졌잖아요. 앗 그럼 weak랑 뭐가 많이 다른 거지.. 하고 간만에 ARC 문서를 읽어보았숩니다. 번역은 나중에하고 본론으로 :0
weak 와 unowned 모두 Reference Count를 증가시키지 않습니다.
weak의 경우 참조된 객체가 deinit 될 경우 ARC에 의해 nil 로 변경됩니다.
하지만 unowned는 deinit 되어도 ARC가 nil로 변경해주지 않습니다.
그래서 이미 해제된 객체에 접근하려고 하기 때문에 BAD ACCESS
runtime error가 발생합니다.
unowned의 정의 자체는 동일한데, Type이 Optional 이 되었다는 것은 무엇이냐. 예전에는 weak와 unowned의 가장 큰 차이가 Optional인지 여부였습니다. 하지만 이제 unowned도 Optional Type이 가능해졌죠. 그 말은 의도적으로 nil 인 경우도 가능하다는 의미인데, 한국어가 어려우니 예시를 들어봅시다.
대학교를 생각해봅시다. 각 학부는 정해진 전공 과목들이 있고, 각 과목에는 후행되는 과목 (DB1 -> DB2 -> DB 심화)이 있을 수 있습니다. 물론 없을 수도 있죠.
이걸 다음과 같이 정의해봅시다.
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
하지만 이 과목들은 부서가 없다면 존재하지 않습니다. 공통 교양과목이 아니라 전공과목이라 생각해보자구요. 그래서 department는 course보다 길거나 같은 생명주기를 따릅니다. 과목은 있거나 없어질 수 있지만, 전공 과목인데 전공 부서가 없는 건 이상하잖아요.
그래서 unowned
로 정의해주었습니다.
마찬가지로 후행 과목도, DB1이 없는데 DB2 가 있는 건 이상하니까 후행 과목이 선행 과목보다 길거나 같은 생명주기를 가진다고 생각합니다. department
와 같이 동일 논리로 unowned
로 정의해주고 싶은데, 문제는 후행 학습이 있을 수도 있고, 없을 수도 있다는 거죠 :0 그래서 Optional<Course>
로 후행 학습의 타입을 결정해주었습니다.
let department = Department(name: "Horticulture")
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
이런식으로 unowned relationship을 구성해줄 수 있습니다.
마지막 과목인 advanced
는 후행 과목이 없기 때문에 셋해주지 않았습니다.
이들간의 관계도는 다음과 같습니다.
이렇게 보면 unowned
가 Optional이 될 수 있는 것이 뭔가 당연한데, Swift 5 이전에는 이런 경우에 weak
를 썼던 걸까요? :0? 궁그미.. 누군가는 필요하다고 생각하니 추가되었겠지요.
사실 어떤 객체와 다른 객체의 관계에 대한 생명주기를 확신하기 어려워 unowned
는 잘 쓰지 않았었는데, 용기를 가지고 시도해봐야겠어요.
결국 weak와 unowned의 가장 큰 차이! 는 이제 ARC
에 의해 nil로 변경이 되냐 안 되냐의 차이만 남았네요.
오히려 더 clear~~ 해진 느낌 ><
그럼 이만 뿅!
ReplayKit을 사용하면, 사용자는 화면의 영상과 앱과 마이크의 오디오를 녹화할 수 있다. 이를 통해 게임에 대한 음성 해설을 제공할 수 있고, 시스템 공유 시트를 사용하여 공유할 수 있다.
ReplayKit은 HD급 비디오를 제공합니다.
애플리케이션에서 운영체제를 통해 레코딩을 시작하려면 RPScreenRecorder을 통해 녹화를 시작할 수 있다. 녹화가 시작되면 Replay Daemon 으로 메세지가 전달되고 앱의 데이터를 Movie file에 쓰기 시작한다.
애플리케이션이 녹화를 중지하면 Replay Daemon이 쓰기를 종료하고 완성된 Movie file을 Application 단으로 돌려주기 위해 RPPreviewViewController
를 제공한다.
이 뷰 컨트롤러를 통해 사용자들은 영상을 미리 보면서 편집하고, 공유할 수 있다.
녹화를 시작 / 중단 / 취소 현재 녹화할 수 있는지 체크할 수 있음
녹화 가능 여부가 변경되는 시점을 알 수 있음 오류가 발생하여 녹화가 중지되었을 때를 알 수 있음
사용자에게 녹화영상을 미리볼 수 있는 뷰 컨트롤러를 제공 편집과 잘라내기, 공유
프리뷰가 종료되었을 때의 시점을 알 수 있다.
private lazy var sharedRecorder = RPScreenRecorder.shared()
func didPressRecordButton() {
sharedRecorder.startRecording { error in // 녹화 시작
if error == nil {
self.showIndicatorView(text: "Recording")
}
}
}
// 녹화중이라는 인디케이터를 보여줍시다.
func showIndicatorView(text: String) {
recordingIndicatorWindow = UIWindoe(frame: UIScreen.main().bounds) // 별도 윈도우에 만들어서 영상에 포함되지 않음 ㅇㅁㅇ?
recordingIndicatorWindow?.isHidden = false
recordingIndicatorWindow?.backgroundColor = UIColor.clear()
recordingIndicatorWindow?.isUserInteractionEnabled = false
let indicatorView = IndicatorView(text: text)
recordingIndicatorWindow?.addSubview(indicatorView)
}
// 중지 버튼을 눌렀습니다!
func didPressStopButton() {
sharedRecorder.stopRecording { previewViewController, error in // 녹화가 종료되면 previewViewController 객체가 함께 넘어옴
self.hideIndicatorView()
if error == nil { // error 없이 종료되었다면 프리뷰 화면을 보여줄 것.
self.previewController = previewViewController
self.previewViewController?.previewControllerDelegate = self
}
}
// 메뉴가 뜬 상태에서, 공유를 눌렀을 경우
func didPressShareButton() {
if let preview = previewViewController {
preview.mode = .share /// 미리보기는 preview
self.present(preview, animated: true)
}
}
// 사용자가 프리뷰 화면에서 되돌아 오려고 할떄
// RPPreviewViewControllerDelegate 내부 함수입니다.
func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
previewController.dismiss(animated: true) // 옹 이쪽에서 닫아줍니다
}
다음 녹화가 시작되면 이전 녹화영상은 제거된다.
RPScreenRecorder.discardRecording()
을 사용할 것비디오와 오디오 컨텐츠를 녹화할 수 있다.
플레이어는 직접 iOS 또는 TVOS 장치에서 타사 스트리밍 서비스에 게임 플레이를 생방송할 수 있다. iOS에서는 마이크와 카메라를 제공하여 FaceTime 카메라를 사용해 해설을 제공할 수도 있다. 컨텐츠는 broadcast service를 통해서만 접근 가능하며 매우매우 안전하다.
func didPressBroadcastButton() {
RPBroadcastActivityViewController.load { broadcastAVC, error in // broadcast service 를 제공하는 앱을 선택할 수 있는 화면 표출해주기 (share extension이랑 비슷한 흐름)
if let broadcastAVC = broadcastAVC {
broadcastAVC.delegate = self
self.present(broadcastAVC, animated: true)
}
}
}
// 서비스를 선택하고 / 혹은 선택하지 않고 닫았을 때
// RPBroadcastActivityViewControllerDelegate
func broadcastActivityViewController(
_ broadcastAVC: RPBroadcastActivityViewController,
didFinishWith broadcastController: RPBroadcastController?,
error: NSError?) {
broadcastAVC.dismiss(animated: true) {
self.startCountDownTimer { // 아니 broadcastController 이게 있는지 먼저 체크해야하는 거 아닌가
broadcastController?.startBroadcast { error in
// broadcast started!
}
}
사용자에게 현재 broadcasting 중이라는 것을 알려주는 것이 중요함 ;)
broadcastController?.isBroadcasting
플래그를 사용할 것func didPressBroadcastButton() {
self.broadcastController?.finishBroadcast { error in
if error == nil {
// broadcast finished!
self.updateBroadcastUI()
}
}
}
// RPBroadcastActivityViewControllerDelegate
func broadcastActivityViewController(
_ broadcastActivityViewController: RPBroadcastActivityViewController,
didFinishWith broadcastController: RPBroadcastController?,
error: NSError?) {
self.broadcastController = broadcastController
// set a delegate to be notified of errors
self.broadcastController?.delegate = self
}
//RPBroadcastControllerDelegate
func broadcastController(
_ broadcastController: RPBroadcastController,
didFinishWithError error: NSError?) {
if error != nil {
// error occurred during broadcast
self.showErrorMessage(message: error!.localizedDescription)
// update UI to indicate the broadcast is stopped
self.updateBroadcastUI()
}
}
앱이 백그라운드에서 실행되면 브로드캐스트를 자동으로 일시 중단한다. 따라서 다시 활성화되는 시점에 사용자에게 브로드캐스트를 다시 재개할 것인지 물어봐야 한다.
func applicationWillResignActive() {
// ReplayKit will automatically pause the broadcast
}
func applicationDidBecomeActive() {
if self.broadcastController?.isBroadcasting == true {
self.promptUserToResumeBroadcast { userWantsToResume in
if (userWantsToResume == true) {
// user wants to resume
self.broadcastController?.resumeBroadcast()
self.updateBroadcastUI()
} else {
// user does not want to resume
self.broadcastController?.finishBroadcast { error in
self.updateBroadcastUI()
}
앱 내부에 포함되어 있지만, 다른 애플리케이션 process 에서 실행됨. 상위 애플리케이션과 데이터를 공유할 수 있음 애플리케이션에 비해서는 주어진 리소스가 제한적이다. (효율적이게 구성합시다)
Add Target -> iOS/tvOS -> Application Extension -> Broadcast UI Extension / Broadcast Upload Extension
로그인이나 정보 등록 등 방송에 필요한 모든 전처리 단계가 이 단계에서 해야함. 이용약관을 보여주거나 이 영상을 외부 SNS 에 공유할 수 있는 등 ! 셋업이 완료되면 브로드캐스트 설정이 완료되었음을 알려주어야 한다.
영상 / 음성 데이터를 받아서 처리 서버에 업로드 브로드 캐스트 서비스와 연관된 다른 필요한 요소들을 구현할 것.
FaceTime camera 지원
if let cameraPreview = RPScreenRecorder.shared().cameraPreviewView {
cameraPreview.frame = CGRect(…)
self.view.addSubview(cameraPreview)
}
유연한 마이크 녹음 지원
func enableMic {
RPScreenRecorder.shared().isMicrophoneEnabled = true
}
func disableMic {
RPScreenRecorder.shared().isMicrophoneEnabled = false
}
iOS 10 이상에서 사용 가능~