아주 간단하게 모바일 웹페이지 상단에 앱스토어 배너 넣기

요즘 스마트폰이 활성화 되면서 모바일 서비스를 제공시 iOS, Android 등의 스마트폰용 앱 주력으로 사용하는 경우가 많습니다.
하지만 앱 설치 유도, 접근성 등으로 모바일 웹페이지도 함께 제공을 할 수 밖에 없죠.
물론 주력 서비스는 앱이기 때문에 다양한 방법으로 모바일 웹페이지에서 앱으로의 이동을 유도합니다.

iOS에서는 아주 간편하게 해결 할 수 있습니다.

<meta name=”apple-itunes-app” content=”app-id=앱아이디”>

Meta Tag 하나를 추가하여

이런 형태의 배너를 모바일 웹페이지에 추가할 수 있습니다.

Meta Tag 내에 넣어야 하는 앱아이디는 앱스토어 링크에서 알 수 있습니다.

https://itunes.apple.com/us/app/apple-store/id375380948?mt=8

~store/id 뒤에 있는 것을 사용하면 됩니다. 375380948

 

앱을 설치하기 전에는 위 배너가 앱스토어로 이동되지만 앱을 설치한 후에는 앱으로 열어 줍니다.
앱을 연 주소를 가지고 앱에서 대응을 하려면

AppDelegate.swift 에 application(_:open:options:) 를 추가한 후 URL에 맞게 대응할 수 있습니다.

https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application

 

Advertisements

‘Guideline 2.5.1 – Performance – Software Requirements’ ‘Your app uses or references the following non-public APIs:’ 리젝에 대응하는 방법.

애플에서 허용하지 않는 API를 사용하는 경우에 앱이 리젝 당합니다.
당연히 사용하지 말라고 한 API를 사용한 코드를 제거하고 다시 심사요청을 하면 되겠죠.

그런데 iOS 앱 개발자들은 일반적인 앱을 만들 경우 비허용/비공개 API를 사용할 일이 잘 없습니다.
Xcode에서 검색을 해도 안 나오겠죠.
가져다 쓴 라이브러리나 타 서비스 SDK에서 사용한 경우일 것입니다.
간단하게 외부 바이너리 라이브러리에서 비허용 API를 사용하고 있는 지 확인하는 방법을 알려 드리겠습니다.

프로젝트 폴더 혹은 외부 바이너리 라이브러리가 있는 폴더에서
터미널 명령으로

grep -R ‘애플에서 알려 준 non-public API’ *

예를 들자면 광고 대행사 SDK를 많이 가져다 쓰면 발생할 수 있는 리젝 사유인 LSApplicationWorkspace

grep -R ‘LSApplicationWorkspace’ *

어느 광고 대행사 SDK때문에 리젝된 것인지 알 수 있습니다.

 

 

present()로 띄운 UIViewController의 WKWebView에서 사진 첨부를 하면 UIViewController가 닫혀(dismiss()) 버린다.

present() 로 띄운 UIViewController에 있는 UIWebView/WKWebView에서 input type=”file” / 을 눌러서 사진을 첨부하려 하면 해당 UIViewController까지 함께 닫혀 버리는 문제가 있습니다.

이 문제는 iOS 8.0.2 부터 발생한 문제로… iOS 9는 물론 iOS 10에서도 고쳐지지 않았죠.

이쯤되니 OS 자체 버그가 아니라 의도된 것 같다는 생각이 듭니다. 의도된 것이 맞다면 어떤식으로 개발을 해야 되는 건지 모르겠네요.

그래서 사용한 임시 방법.

dismiss()를 override 해버리는 방법입니다.

// MARK: - Variable
var isDismiss: Bool = false

// MARK: - Override
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    if !self.isDismiss && (self.presentedViewController == nil || self.presentedViewController == self) {
        return
    }

    self.isDismiss = false
    super.dismiss(animated: flag, completion: completion)
}

// MARK: - Function
func close(animated: Bool, completion: (() -> Void)? = nil) {
    self.isDismiss = true
    self.dismiss(animated: animated, completion: completion)
}
// MARK: - Action
@IBAction func close(_ sender: UIButton) {
    self.close(animated: true)
}

편법이지만 제일 간단한 방법이죠…

‘아이슬란드 일주’ 5일만에 끝내기 – 3일차

어젯밤엔 어두워서 몰랐지만 외딴 곳에 있는 숙소의 경치가… 와…

보너스 마트에서 산 아침 거리로 핫도그를 만들어 먹고,

2016-10-05-08-05-35.jpg

경치를 즐기며 여유있게 드립 커피 한 잔.



출발 준비 완료! 




110716_1004_56.jpg

우선 에이일스타디르에서 주유를 하고

본격적으로 출발.

110716_1004_57.png

첫 목적지는 ‘세이디스피외르뒤르’입니다.

110716_1004_58.jpg
산으로 둘러 쌓여 있는 아주 멋진 이 항구 “마을”은 생선 가공업이 주산업이었지만 관광업으로 바꾸었다고 하네요.

그래도 항구 근처에 컨테이너들이 몇 있더군요.

마을을 둘러싼 산, 마을의 목조 건물들이 이미 아름다웠지만 거리 곳곳에서 관광객을 신경 쓴 모습도 볼 수 있었습니다.

110716_1004_59.png110716_1004_60.jpg110716_1004_61.jpg

다음 목적지는 회픈입니다. 점심 먹으러 ㄱㄱ

110716_1004_64.png

회픈 가는 길은 예상보다 오래 걸리고 말았습니다.
갤럭시 s7 엣지 핸드폰으로 네비게이션으로 사용했는데 무슨 이유인지 GPS가 튀면서 방향을 잘못 잡았습니다.

엉뚱한 길로 돌아가는 상황이 되었죠.

다행히 일행 중 한명이 상황은 눈치 챈 덕분에 몇시간을 더 돌아서 갈 상황은 막았습니다.

110716_1004_65.png110716_1004_66.jpg

비도 와서 속도 내기 힘든 상황에서 하루 일정을 다 망칠 뻔 했네요.

네비게이션을 새로 세팅하고 유턴하여 다시 출발.

그런데 한가지 더 난관이 기다리고 있었네요.

북쪽에서도 보지 못한 험한 도로를 만납니다. 1번 국도는 전체적으로 잘 닦인 도로라 생각했는데 곳곳에 구멍이 파진 험한 도로를 만납니다.

거기다…

엉뚱한 길로 가면서 많은 시간을 낭비했던지라 지도만 보고 1번 국도를 포기하고 거리가 가까운 도로를 선택했는데…

어차피 1번 국도도 험하니 벗어나서 다른 도로로 가도 상관이 없을 것 같았지만 잘못된 선택, 하필 산을 타는 도로였습니다.

비도 오는 상황에서 고도가 높아 지면서 안개가 짙어지고 시야가 완전히 가려집니다.

110716_1004_67.png

아이슬란드에서 경험한 최대의 위기가 아니었나 합니다.

한국에 돌아와서 알아 보니 매우 아름다운 풍경이었고 길 옆도 상상한 무시무시한 절벽이 아니었지만 저런 안개 속에선 전혀 알 수 없었죠. 조심 조심. 운전에만 초 집중.

110716_1004_68.jpg

드디어 시야가 확보된 도로.

무사히 회픈 도착.

110716_1004_69.jpg

회픈에 맛집으로 유명한 Kaffi Hornið에서 저녁에 가까운 점심 아닌 점심 식사 후 요쿨살론으로 이동합니다.

20161005_154954

20161005_161118.jpg20161005_162308.jpg20161005_162317.jpg

저녁거리를 회픈에서 마련하려 했지만 시간도 부족하고 점심이 많이 늦었기 때문에 바로 요쿨살론으로 향합니다.

가는 도중 제가 핸드폰 카메라 렌즈를 식당에 두고 와서 다시 되돌아 갑니다.

오늘 여러모로 제 갤럭시 핸드폰이 말썽이네요.

110716_1004_70.png110716_1004_71.jpg

2016-10-05-18-29-052016-10-05-18-34-5220161005_18260620161005_18314720161005_183359

빠르게 해가 지기 시작할때 요쿨살론에 도착합니다.

해가 완전히 지기 전에 도착하여 이런 멋진 빙산들을 볼 수 있어서 다행입니다. 안개 너머 더 많은 빙산들을 보지 못했지만 앞에 보이는 빙산들만으로도 너무 멋집니다.

110716_1004_72.png

요쿨살론을 떠날땐 이미 완전히 해가져서 우리가 피하고 싶었던 야간 운전을 또 하게 되네요.

아이슬란드의 다리는 희한하게도 2차선이 없더군요. 우리가 보았던 다리들은 1차선에, 중간 중간 앞에 오는 차량을 피할 수 있는 공간만 있었습니다.

그런 다리들을 건너 드디어 숙소에 도착.

110716_1004_73.jpg

날씨에… 여러가지 사건도 있어서 다들 피곤한지 저녁 생각 없이 다들 취침.

주석이 필요 없는 코드.key

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-002

주석 자체가 나쁜 것은 아니다. 개발자는 주석을 남기는 것을 귀찮아 한다. 그럼에도 주석을 작성해야 되겠다 생각이 들었다면 그것은 나쁜 코드를 작성하고 보완을 하려는 목적일 가능성이 매우 높다. 코드를 보완하는 것이 훨씬 낫다. 다시 말하지만 주석 자체가 나쁜 것은 아니다. 하지만 주석으로 보완하지 않는 코드를 작성하는 습관이 선행되어야 한다 주석을 잘 다는 습관은 그 다음에 익혀도 된다.

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-003

개발자라면 일정에 쫓겨 나쁜 코드를 작성한 후 나중에 수정해야지 라는 생각을 가진 경험이 있을 것이다. 그리고 개발자라면 그 ‘나중’이 단 한번도 온 적이 없다는 것을 경험으로 알 것이다.

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-004

선언 의도를 파악할 수 있는 이름, 의미가 있는 이름. 불필요하게 단순화 시킨 이름은 검색이 어렵다. 발음이 쉽지 않은 이름은 커뮤니케이션을 어렵게 한다. 동일한 행위에 대해서는 동일한 단어를 사용하는 것이 좋다. get receive fetch -> receive / 그러나 통일성을 위해 강박적으로 단어를 통일 시켜서도 안된다. 동일한 행위라도 차이가 있다면 구분을 해주어야 한다. add insert append

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-005%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-006

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-007

마지막으로 강조하고 싶은 것은 코드는 만들때 잘 만드는 것도 중요하지만 실상 코드는 수정, 추가를 거치면서 나쁜 코드로 가속화 된다. 남이 만든 코드든 자신이 만든 코드든 수정, 추가할때 더욱 유의해야 한다. pull 받은 코드는 push 할때 더욱 깨끗하게.

%ec%a3%bc%ec%84%9d%ec%9d%b4-%ed%95%84%ec%9a%94%ec%97%86%eb%8a%94-%ec%bd%94%eb%93%9c-008

반복된 디버깅에 지쳐 있다면 코드부터 뜯어 고치자. 같은 방식으로 개발하면서 버그가 발생하고 유지보수가 어렵다고 투덜 거리는 건 의미 없다.

WKWebView 사용 시 필수적으로 넣어줘야 하는Delegate 넷.

WKWebView를 사용한다면 아래의 넷은 반드시 넣어 주어야 한다. 복사->붙여 넣기 후 개발을 시작하면 된다.

우선, WKUIDelegate 셋.

func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping () -> Void) {
    let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        completionHandler()
    }))

    self.present(alertController, animated: true, completion: nil)
}

func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (Bool) -> Void) {
    let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        completionHandler(true)
    }))
    alertController.addAction(UIAlertAction(title: "취소", style: .default, handler: { (action) in
        completionHandler(false)
    }))

    self.present(alertController, animated: true, completion: nil)
}

func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void) {
    let alertController = UIAlertController(title: "", message: prompt, preferredStyle: .alert)
    alertController.addTextField { (textField) in
        textField.text = defaultText
    }
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        if let text = alertController.textFields?.first?.text {
            completionHandler(text)
        } else {
            completionHandler(defaultText)
        }
    }))

    alertController.addAction(UIAlertAction(title: "취소", style: .default, handler: { (action) in
        completionHandler(nil)
    }))

    self.present(alertController, animated: true, completion: nil)
}

이건 iOS 9.0 이후 지원이긴 한데… WKNavigationDelegate 하나.

public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
    // 중복적으로 리로드가 일어나지 않도록 처리 필요.
    webView.reload()
}

앱이 백그라운드 상태에서 포그라운드로 돌아 올때 간혹 WKWebView가 하얀 화면으로 되어 있는 경우가 있는데, 보통 자바스크립트 등으로 체크해서 대응을 하고 있지만 iOS 9이상에선 지원하고 있다.

‘아이슬란드 일주’ 5일만에 끝내기 – 2일차

2일차. 오늘도 달려야 합니다. 오늘 일정은 약 330km로 어제보다는 양호하지만 여전히 먼 길입니다. 거기다 아이슬란드 동쪽으로 넘어 가면서 관광 명소가 하나씩 나오기 시작합니다. 중요한 곳을 몇 추려서 방문을 해야 합니다. 시간은 확실히 어제 보다 부족할 지도 모르겠네요.

한국에서 가져 온 햇반, 캔 반찬 등으로 아침을 떼우고 아침 일찍 출발합니다.


첫 목적지는 바로 신들의 폭포라는 뜻의 ‘고다포스’

폭포의 나라이기도 한 아이슬란드에서 처음 마주하게 된 큰 폭포, ‘과연 신의 폭포!’ 라는 생각이 들 정도로 아름다웠습니다.

110716_1004_28.jpg110716_1004_29.jpg110716_1004_30.jpg

고다포스의 이름에는 여러 설이 있지만 그 중에는 종교적인 전설도 있다고 합니다.
약 천년 전 아이슬란드의 국교가 기독교로 공표되자 사람들이 이교도 신상과 숭배를 금지하는 기독교 교리에 의해 기존 북유럽 신들의 상들을 이 폭포에 버렸었다고 하네요.
그게 사실이라면 저 밑에 천년된 유물들이!

다음 목적지는 노천온천인 ‘뮈바튼 자연 온천’입니다.


한국에서 사전에 오픈 시간을 조사하고 도착하였으나… 우리가 조사한 시간은 아이슬란드의 매우 짧은 하절기 시간이었기 때문에 아직 온천이 오픈하지 않았더군요.


계획 변경!

먼저 데티포스를 다녀 오기로 합니다. 영화 ‘프로메테우스’에서도 나왔던 아이슬란드 최대 크기의 폭포. 고다포스가 아름답다면 데티포스는 크기와 회색빛으로 압도하는 웅잠함을 가지고 있죠.

한국에서 부터 기대를 가졌던, 그 웅장함을 보러 갑니다.




도착하였으나.. 횡합니다. 폭포가 보이지 않습니다…
고다포스는 차로 진입을 할 수 있었으나 데티포스는 주차장에서 약 10여분간 걸어서 이동해야 합니다.


우비로 무장하고 출발.

이동하는 중에도 계속 경이로운 모습을 보여 주는 아이슬란드…


 

도착!!!!!!!!



찍었던 사진들을 아무리 뒤져도 데티포스의 포스를 느끼게 해줄 사진은 찾을 수 없군요.

아쉽지만… 데티포스를 뒤로 하고 다시 뮈바튼 온천으로 이동합니다.


오픈 시간이라 그런지 아까와 달리 우리 외에도 다른 관광객들이 많네요.

뮈바튼 온천은 자연 온천이기 때문에 온도가 일정하지 않아요. 그래서 들어 가면 기대 했던 온천의 뜨거움 보다는 미지근하기 때문에 실망을 하게 되는데… 잘 찾아 보면 온도가 높은 곳도 있었습니다.

이미 관광객들이 많이 있었기 때문에 그런 곳을 찾는 노력을 하진 못했네요.

자 이제 온천을 즐기고 몸의 피로도 씻어 냈으니 다시 일주를 시작하겠습니다.

에이일스타디르에 잠시 들려서 저녁 식사를 하고 내일 아침에 먹을 거리를 장보고 숙소로 이동하도록 하죠.



이동하는 중간 중간 차 밖을 아무렇게나 찍어도 그림이 찍힙니다.
도로가에서 발견한 이름 모를 폭포도 그림입니다.


에이일스타디르 도착.

맛난 저녁을 먹고 근처에 있던 ‘보너스’ 마트로 향합니다.
오늘 숙소는 외딴 곳이라 아침 식사 거리를 장보기 위해서였죠.


이름 처럼 우리에게 보너스였던 보너스 마트.

한국보다도 비싼 차량 주유비, 식당에서 먹으면 한끼 3만원 가량하는 밥값.
살인적인 물가에 움추려든 우리 지갑에 보너스 마트의 저렴한 식재료 가격은 우리에게 정말 보너스였습니다.

이제 숙소로 향합니다.

에이일스타디르에서 이미 해가 지기 시작해서 숙소에 도착했을 때는 이미 어두컴컴했습니다.


주위에 아무것도 없이 숲속에 오두막 하나…

주위에 아무것도 보이지 않는데 집 한 채만 달랑 있으니 살짝 으스스한 분위기입니다.

하지만 덕분에 별이 잘 보입니다. 어쩌면 오늘 오로라를 볼 수 있을 지도 모르겠네요.

그런데… 새벽이 되니 비가옵니다. 집이 흔들릴 정도로 바람도 불어대구요.

아늑하고 따뜻한 집이었는데, 아이슬란드의 바람 앞에선 이 집도 어쩔 수 없었죠.

결국 오늘도 오로라 헌팅은 실패. 가장 가능성이 컸던 날인데…

이후로도 오로라 헌팅은 계속 실패했으니 가장 가능성이 있었던 날을 비때문에 망친셈이네요.

110716_1004_51.jpg110716_1004_52.jpg