XCodeからiTunes Connectへアップロード後に`has change to Invalid Binary`エラー

XCodeからiTunes Connectへアーカイブファイルをアップロードした後にhas change to Invalid Binaryエラーが発生した場合は、問題の詳細がメールで送信されてくる。

Missing Push Notification Entitlement

Missing Push Notification Entitlementは、Apple Developer > Certificates, Identifiers & Profiles > App IDsでPush Notificationsが有効化されていないか、もしくは、XCode -> TARGET -> Capability -> Push NotificationsでPush Notificationsが有効化されていないかのいずれかの場合が多いため、これらの設定を見直した上で再度アップロードを行う。

Missing Push Notification Entitlement – Your app includes an API for Apple’s Push Notification service, but the aps-environment entitlement is missing from the app’s signature. To resolve this, make sure your App ID is enabled for push notification in the Provisioning Portal. Then, sign your app with a distribution provisioning profile that includes the aps-environment entitlement. This will create the correct signature, and you can resubmit your app. See “Provisioning and Development” in the Local and Push Notification Programming Guide for more information. If your app does not use the Apple Push Notification service, no action is required. You may remove the API from future submissions to stop this warning. If you use a third-party framework, you may need to contact the developer for information on removing the API.

Certificatesの修正が必要な場合

XCode上で修正が必要な場合

privacy-sensitive data without a usage description

iOS10よりユーザデータにアクセスする場合は、使用目的をInfo.plistに記述する必要がある。

We have discovered one or more issues with your recent delivery for “YOUR APP NAME”. To process your delivery, the following issues must be corrected: This app attempts to access privacy-sensitive data without a usage description. The app’s Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

Info.plistに記述した文言は、データアクセスの許可を求めるポップアップ上に表示される。NAVITIMEアプリの場合はこんな感じ。

AWS S3(2)S3Sync:S3バックアップツール

S3Sync

以前、S3にバケットを作成してGlacierアーカイブを行う手順を確認したが、この仕組みを利用してMacの任意のディレクトリをS3 Glacierと自動的に同期するアプリケーション「S3Sync」を作ってみた。Macのスリープを検知すると同期を始めるので、寝ている間にラクラク同期できる。

といってもこのアプリ、単にNSTaskを使ってシステムコマンドを実行しているだけのアプリなので、任意のコマンドを自由に実行することができる。ステータスバーに常駐しているアプリなので、作業の邪魔にもならない。

S3Sync

スリープ検知

スリープ検知をするには、NSWorkspace ClassNSWorkspaceWillSleepNotification属性を使う。

    func applicationDidFinishLaunching(aNotification: NSNotification) {
        // スリープ検知
        NSWorkspace.sharedWorkspace().notificationCenter.addObserver(self, selector: #selector(self.receiveSleepNotification(_:)), name: NSWorkspaceWillSleepNotification, object: nil)
    }

    func receiveSleepNotification(notification: NSNotification){
        // スリープ実行時に行う処理
    }

システムコマンドの実行

システムコマンドは、NSTask Classから実行することが可能である。
NSTaskは、実行したシステムコマンドの出力結果を取り出すことも可能だが、readDataToEndOfFile()を使うとブロッキング処理が発生してしまうので、dispatch_async()を使って非同期に順次出力処理していく必要がある。

let task = NSTask() 
// 実行コマンドをフルパスで指定
task.launchPath = "/usr/local/bin/aws "
// パラメータを配列形式で指定
task.arguments = ["-h", "hogehoge"]
// 標準出力をパイプに渡す
let pipe: NSPipe = NSPipe()
task.standardOutput = pipe
let stdoutHundle = pipe.fileHandleForReading     
// 非同期処理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), {
var dataRead = stdoutHundle.availableData
while(dataRead.length > 0){
	let stringRead = NSString(data: dataRead, encoding: NSUTF8StringEncoding)
        if let output = stringRead {
		// 出力結果処理
        }
        dataRead = stdoutHundle.availableData
}
// コマンドの実行
task.launch()

NSTaskは、suspend()terminate()を使って、途中で処理を停止したり、完全に終了してしまったりすることができる。suspend()で中断した処理は、resume()で再開することができる。また、タスクが実行中にも関わらず再度launch()を実行してしまうと、下記の実行エラーが発生してしまう。

task already launched

通知の送信

本アプリは、コマンド実行毎にMacの通知センターに実行状況を通知する。

アプリ通知

Macの通知センターに通知を送信するには、NSUserNotificationCenter Classを使う。送信する通知には、「タイトル」「サブタイトル」の他に様々な項目を設定することが可能である。

// NSUserNotificationCenterDelegateが必要
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {

    func deliverNotification(title : String, subtitle : String, informativeText: String){
        // AppDelegate Classにデリゲードを指定
        NSUserNotificationCenter.defaultUserNotificationCenter().delegate = self
        let notification = NSUserNotification()
        notification.title = title
        notification.subtitle = subtitle
        notification.informativeText = informativeText
        notification.contentImage =  NSImage(named: "MainIcon")
        notification.userInfo = ["title" : "タイトル"]
        NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(notification)
    }

}

ログを保存するディレクトリの指定

本アプリは、実行ログをファイルに保存することができる。

NSOpenPanel

Macでディレクトリやファイルを開くする際は、NSOpenPanel Classを利用する。
また、保存の際はNSSavePanel Classというクラスも用意されている。

// MARK: ディレクトリ選択画面
let panel = NSOpenPanel()
// ファイル選択の可否
panel.canChooseFiles = false
// ディレクトリ選択の可否
panel.canChooseDirectories = true
// 複数選択の可否
panel.allowsMultipleSelection = false
panel.beginWithCompletionHandler({(num) -> Void in
      if num == NSModalResponseOK {
           // ディレクトリ・ファイル決定時の処理
      }
})

常駐アプリ

ステータスバーに常駐するアプリを作成するためには、Project > TARGET > Info > Custom OS X Application Target Propertiesから、Application is agent (UIElement)YESに設定する。

Application is agent

StoryBoardのTips

  • 常駐アプリであっても、Main Menu > Edit がないと、TextFieldの Shortcut Keyが使えない

Main Menu : Edit

‘openssl/pkcs7.h’ file not found への対応

‘openssl/pkcs7.h’ file not found

XCode 7からOpenSSLライブラリを使用する場合に、‘openssl/pkcs7.h’ file not foundという警告が出ることがある。これはOpenSSLのライブラリがプロジェクト内に不足しているためで、XCode7でOpenSSLを使用する場合は、CocoaPodsからOpenSSLをダウンロードして使用すると良い。

CocoaPodsのインストール

  • まずは、Xcodeバージョン管理ツールの1つであるCocoaPodsをMacにインストールする。

gemのアップデート

sudo gem update --system

CocoaPodsのインストール

CocoaPodsのセットアップ

pod setup

以上で、XCodeプロジェクト管理ツールCocoaPodsのインストールが完了する。

OpenSSLのインストール

CocoaPodsはPodfileと呼ばれる設定ファイルに、インストールの内容を設定することでインストールを実行することが可能である。XCodeでSSLを使用するには、CocoaPodsからOpenSSL-Classicをインストールする。

プロジェクトファイルに移動

cd PROJECT_DIRECTORY

Podfileを作成

source 'https://github.com/CocoaPods/Specs.git'
pod 'OpenSSL-Classic', '1.0.1.j'

OpenSSLライブラリをインストール

pod install

+ここで当該のXcode Projectを開いていると以下のように警告されるので、Xcodeは閉じてからインストールする

[!] Please close any current Xcode sessions and use `XXXXX.xcworkspace` for this project from now on.

CocoaPodsからライブラリをインストールするとXXXXX.xcworkspaceというファイルが作成されるので、XXXXX.xcodeprojではなくXXXXX.xcworkspaceをクリックして起動する。また、ターゲットの設定で、HEADER_SEARCH_PATHSに$(inherited)を追加する。

WKWebViewの使い方

WKWebView

WKWebViewはiOS8から新たに使用できるようになったWebViewで、WebKitFrameworkの中に含まれている。これまでのUIWebViewと比べると速くて高機能とのこと。使用方法は以下の通り。

WKWebViewの初期化

最初に初期化しておく。

private var webView = WKWebView()

各種設定

AutoLayout、Delegateなどの設定をviewDidLoad()で行う。デフォルトではジェスチャーが無効なので、有効にするためにはコードで明示する必要があることに注意が必要である。

    override func viewDidLoad() {
        // Autolayoutを設定
        webView.translatesAutoresizingMaskIntoConstraints = false
        // 親ViewにWKWebViewを追加
        self.view.addSubview(webView)
        // Delegateの設定
        self.webView.UIDelegate = self
        self.webView.navigationDelegate = self
        // WKWebViewを最背面に移動
        self.view.sendSubviewToBack(webView)
        // レイアウトを設定(後述)
        setWebViewLayoutWithConstant(0.0)
        // ジェスチャーを許可
        webView.allowsBackForwardNavigationGestures = true
        // ページのロード
        self.webView.loadRequest(NSURLRequest(URL: NSURL(string: url)!))
    }

終了時

親ビューから削除する。Autolayoutの値もリセットされる。

    override func viewDidDisappear(animated: Bool) {
        webView.removeFromSuperview()
    }

レイアウト設定

WKWebViewは、StoryBoard上で割り付けることが出来ない。他のパーツをStoryBoard上でAutolayout設定している場合は、Swift上でWKWebViewにAutolayout設定を行う。ConstraintはWebViewに対して指定するのではなく、親Viewに対して指定することに注意が必要である。

    func setWebViewLayoutWithConstant(constant: CGFloat){
        // Constraintsを一度削除する
        for constraint in self.view.constraints {
            let secondItem: WKWebView? = constraint.secondItem as? WKWebView
            if secondItem == self.webView {
                self.view.removeConstraint(constraint)
            }
        }
        // Constraintsを追加
        self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Width, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.topLayoutGuide, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0))
        self.view.addConstraint(NSLayoutConstraint(item: self.bottomLayoutGuide, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: webView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: constant))
    }

以上により、ViewController上でWKWebViewを表示させることができる。goBack()やreload()はUIWebViewと同様に使用できる。

戻るボタンの実装

前のページに戻るためには、goForward()ではなぜかうまく動かなかった。以下のようにするとうまく動く。

let url = webView.backForwardList.forwardItem?.URL
webView.loadRequest(NSURLRequest(URL: url!))

プログレスビューの実装

ページ読み込みの進捗状況を表示するプログレスビューを表示することもできる。プログレスビュー自体のレイアウトや表示位置は、StoryBoard上で設定し、表示処理をSwiftで記述すると簡単である。

    // プログレスビュー描画間隔
    private let timerDur = 0.1

    override func viewDidAppear(animated: Bool) {
        // プログレスビューの描画
        progressView.setProgress(0.0, animated: false)
        progressView.hidden = false
        timer = NSTimer.scheduledTimerWithTimeInterval(timerDur, target: self, selector: "updateProgressView", userInfo: nil, repeats: true)
    }

    override func viewDidDisappear(animated: Bool) {
        if timer?.valid == true {
            timer?.invalidate()
        }
    }

    /**
        プログレスビューを更新する
    */
    func updateProgressView() {
        progressView.setProgress(Float(webView.estimatedProgress), animated: true)
        if webView.estimatedProgress == 1.0 && timer?.valid == true {
            timer?.invalidate()
            progressView.hidden = true
        }
    }

新規タブで開く

WKWebviewでURL読み込み時に任意の処理を実行したい場合には、decidePolicyForNavigationAction関数内に処理を記述する。新規タブで開く(target = _blank)の指定があった場合にデフォルトでは動作しないので、以下の記述が必要である。

if navigationAction.navigationType == WKNavigationType.LinkActivated{
                if targetFrame == nil {
                    webView.loadRequest(NSURLRequest(URL: url))
                }
            }

UIActivityの使い方

UIActivityとは

UIActivityは、画面下からModal(CoverVertical)で出現する、FacebookやTwitterなどのSNS共有やメール添付の機能をアイコン一覧で表示するActivityである。任意の機能やアイコンを追加する場合は、UIActivityを継承したクラスを作成する必要がある。

UIActivity

カスタムUIActivityの作成

UIActivityを継承したクラスには、「表示するタイトル」や「アイコンイメージ」、「ボタンを押したときの動作」などを記述する。下の例は、「Safariで開く」というアイコンと機能をUIActivityに追加した場合のコードである。

import UIKit

class SafariActivity: UIActivity {
    
    var url: NSURL? = nil
    
    // 表示するタイトル
    override func activityTitle() -> String? {
        return "Safariで開く"
    }

    // 表示するアイコン    
    override func activityImage() -> UIImage? {
        return nil
    }
    
    // どのようなアイテムが投入された場合に機能が働くかを定義する
    // 機能が動作する条件の場合には true を返す
    override func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool {
        for activityItem in activityItems {
            // NSURLかつURLを開くことが出来る場合は動作する
            if activityItem.isKindOfClass(NSURL) && UIApplication.sharedApplication().canOpenURL(activityItem as! NSURL) {
                    return true
            }
        }
        return false
    }

    // 機能が動作する直前の処理を記す    
    override func prepareWithActivityItems(activityItems: [AnyObject]) {
        for activityItem in activityItems {
            if activityItem.isKindOfClass(NSURL){
                url = activityItem as? NSURL
            }
        }
    }
    
    // 機能のふるまいを記述する
    override func performActivity() {
        // SafariでURLを開く
        UIApplication.sharedApplication().openURL(url!)
        self.activityDidFinish(true)
    }
    
}

上記機能を含んだUIActivityを表示するには、以下のように定義したクラスを呼び出す。

// Data Item
let items = [NSURL(string: webView.URL!.description)!]
// Application Activity
let activities = [SafariActivity()]
// 上記2つを引数に指定しUIActivityViewControllerを生成
let activityController :UIActivityViewController = UIActivityViewController(activityItems: items, applicationActivities: activities)
UIActivityViewControllerを表示
self.presentViewController(activityController, animated: true, completion: nil)

以上でカスタムのUIActivityを表示することが可能となった。

C言語とObjective-C

ファイル構成

クラスファイルはヘッダファイルとソースファイルより構成される。

// SampleCode.h
@interface SampleCode
@end
// SampleCode.m
#import "SampleCode.h"
@implement SampleCode
@end
  • クラスファイルはヘッダファイルとソースファイルより構成される
  • ヘッダファイルには、外部に見せるメソッド(インタフェース)を記述する
  • ソースファイルに、実際のメソッドを実装する

プリプロセッサ

数値や文字列に名前を付けて定数を定義することが出来る

#define TAX_RATE 0.05

ヘッダファイルを読み込む

// 標準ライブラリのみ取り込み
#import <UIKit/UIKit.h>
// カレントディレクトリを検索し、無ければ標準ライブラリを検索する
#import "CustomLib.h"

データ型

自分で定義した列挙型(定数のリスト)の例

// 自身で定義した列挙型をViewState型と定義する
typedef enum {
ViewStateHidden
ViewStateVisible
} ViewState;

メソッド

引数の説明(メッセージキーワード)を記述するのが特徴

-(戻り値の型)メソッド名:(引数1の型)引数1の名前 引数2の説明文:(引数2の型)引数2の名前

インスタンス化の不要なクラスファイルは頭に+をつける

+(戻り値の型)メソッド名:(引数1の型)引数1の名前 引数2の説明文:(引数2の型)引数2の名前

その他

  • selfは自分自身のオブジェクトを意味する
  • initはコンストラクタ(初期化)
  • init内では self = [super init]などとしスーパークラスの初期化を行う。returnではselfを返す

処理の進行を表示する

UIActiveIndicatorView

Storyboad上で「Animating」と「Hides When Stopped」にチェックを入れておくとよい

[_indicator startAnimating];

インジケータのアニメーションが開始される

[_indicator stopAnimating];

インジケータのアニメーションが停止される。Storyboad上で「Hides When Stopped」オプションにチェックを入れている場合は、インジケータが非表示となる

UILabel

##文字を折り返す
複数行に渡って表示する場合は、UILabelのnumberOfLinesプロパティを変更する。無制限の場合はゼロを設定する

textLabel.numberOfLines = 0;

折り返すときのモードも設定できる

textLabel.lineBreakMode = UILineBreakModeCharacterWrap;

UITableViewの使い方

テーブルデータの再読み込み

データの更新などを行った後、スレッド内で単に

tableView.reloadData()
[(UITableView)tableView reloadData]

とやると落ちてしまう。そこで以下のようなコードを記述することによって、データの再読み込み処理をメインスレッドに戻して処理させるようにする。

dispatch_async(dispatch_get_main_queue(), { () -> Void in
    self.tableView.reloadData()
})
[(UITableView)tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];

また、下記のようにUserDefaultのデータをCellにデータを反映させる場合に、

@IBOutlet weak var label: UILabel!

override func viewWillAppear(animated: Bool) {
        let defaults = NSUserDefaults.standardUserDefaults()
        label.text = defaults.valueForKey("label")! as? String
}

viewWillAppear()でreloadData()しただけでは、うまくテーブルが更新されないので、

override func viewWillAppear(animated: Bool) {
        let defaults = NSUserDefaults.standardUserDefaults()
        label.text = defaults.valueForKey("label")! as? String
        tableView.reloadData()
}

viewDidAppear()でもreloadData()する必要がある。

    override func viewDidAppear(animated: Bool) {
        tableView.reloadData()
    }

セルのハイライトを解除する

UITableViewControllerを使用しない場合、View遷移時のセルハイライトの解除を自分で記述する必要がある

tableView.deselectRowAtIndexPath(tableView.indexPathForSelectedRow, animated: true)
[(UITableView)tableView deselectRowAtIndexPath:[(UITableView)tableView indexPathForSelectedRow] animated:YES];

セル選択時にハイライト表示しない

セルを選択した場合もハイライト表示としない

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
        cell.selectionStyle = UITableViewCellSelectionStyle.None
        return cell
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	// セル選択時のスタイルを無表示スタイルに設定する
	cell.selectionStyle = UITableViewCellSelectionStyleNone;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	// セル選択時も非選択状態とする
	[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

セル選択不可

UITableView内全てのセルを選択不可にする

tableView.allowsSelection = false
[(UITableView)tableview setAllowsSelection:NO];

ヘッダビューを追加する

UITableView上部にヘッダビューを追加する

tableView.tableHeaderView = view
(UITableView)tableView.tableHeaderView = (UIView)view;

追加するViewは、ViewcontrollerのサブViewにしない。
文字数に合わせたセルの高さを設定する

セルの高さを設定する

  • テーブル内のUILabelの文字数に合わせてセルの高さを指定する
    - (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath  
    {
    	// セルの取得
    	UITableViewCell *cell = [self tableView:_tableView cellForRowAtIndexPath:indexPath];  
    	// 最大サイズ
    	CGSize bounds = CGSizeMake(_tableView.frame.size.width, _tableView.frame.size.height);
    	// ラベルサイズ
    	CGSize size = [cell.textLabel.text sizeWithFont: cell.textLabel.font
    	constrainedToSize: bounds
    	lineBreakMode: UILineBreakModeCharacterWrap];
    	return size.height;
    }
    

  • セルの高さを自動設定する

    // とりあえず高さを指定
    tableView.estimatedRowHeight = 100
    // 高さを自動指定
    tableView.rowHeight = UITableViewAutomaticDimension
    

プロトタイプセルに含まれる子ビューを指定する

Storyboard上で設定したPrototypeCellに含まれるUIImageViewやUILabelをコード上から指定する場合、それぞれのViewにラベルを付与しておくことで、ラベル番号を指定してViewを取得することが可能である。

Prototype Cells

プロトタイプセル上にUIImageViewを作成し、Tag=1を指定している。

Prototype Cells Property

上記の例で、UIImageViewにSwift上からアクセスする場合は、

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
        let imageView: UIImageView = cell.viewWithTag(1) as! UIImageView
        return cell
    }

とすれば良い。