2018年4月20日金曜日

新しいiPhoneの登録

実機確認したい!

とうとうねiPhone買ったんですけど、実機確認のための登録ってどうやるんだっけ? ってことになったんですよ。
iPad2からiPad Air2にした時は特に何もしなかったような気がしたんですけど、iPhoneではちゃんとApple Developerサイトで登録してやらんといかんようなんですよ。

Developerサイトにログインし、AccountタブのCertificates, Identifiers & Profilesを開き、DecicesからiPhoneを選び、右上の「+」ボタンを押すと、NameとIdentifierを入力する画面になるわけですね。
そこで、XcodeのWindowメニューにある「Windows & Simulators」を選ぶと出てくる画面で、デバイス名とIdentifierをそれぞれNameとUUIDに入れてやって下の方にあるボタンを押してやるといいのですよ。
できたからいいけどさ、こういうのもXcodeで統一的に扱えるようにならんもんかね? いちいちApple DeveloperサイトやiTunes Connectのサイトを行ったり来たりじゃめんどくさいよね。

困ったエラー:Could not insert new outlet connection

問題

Outletが接続できなくなるエラーが発生。
Actionもダメみたい。


対処方法

以下のように自分で@IBOutletのコードを入力し、そこに対して接続すれば使える。

@IBOutlet weak var test: UIBarButtonItem!

根本的解決方法はあるのか?

ただし根本的な解決にはなっていないので、以下を試してみた。

  • プロダクトのClean (⌘+Shift+K)
  • DerivedData(プロダクトのビルドフォルダーにある中間ファイル)の削除
    • 参考/[Xcode][小ネタ] DerivedDataの削除についての備忘録
    • 一部の削除(⌘+Option+Shift+K)
    • 丸ごと削除(再作成されるので消しても平気)
      • File / Project Settings... で出るダイアログの Derived Data:の下にある/Users/ユーザ名/Libr.../Xcode/DerivedData ⇨ の⇨を押して開くフォルダ中の、DerivedDataフォルダの中身の当該プロジェクトのフォルダ、もしくはDerivedDataフォルダ丸ごと消してもいい。容量でかいのでメディアの節約にもなる。
  • Xcode再起動
  • Macの再起動
  • こちらに書いてある、Xcode関係のplistをターミナルからいじる

しかし、いずれをやってもダメだった。
うまくいく場合、いかない場合があるようだ。
他のプロジェクトでは問題ないようなので、プロジェクトファイルのどこかが壊れちゃったんだろうね。

このままじゃ不便なので、今後も継続して調べる。わかったら追記する。

2018年4月18日水曜日

macOS/画像ファイルの保存

iOSもいいけど、macOSのアプリも作ってみたいというので勉強始めた。

CIFilterをかけた画像を保存するアプリ。
ファイル保存のウィンドウNSSavePanelを表示したいが、ネットで調べたコードじゃエラーが出て表示されない。

わかったのは、最近Sandboxという仕組みになり、そのせいでファイルの扱いがReadOnlyになっていたのだ。
TARGETSのCapabilities / App Sandbox / File Access / User Selected File のPermission & Accessを、Read Only → Read/Write に変更してやったら(App SandboxのボタンがOFFになる)NSSavePanelが表示され、画像の保存もできた。
めんどっちーね。

なお、User Selected FileがRead/Writeになっていれば、App SandboxのボタンをONにしても保存できるようだ。やっていいのかどうかしらんけど。

Sandboxとはなんぞや?

どうやら砂場を意味していて、子供をその中で遊ばせときゃ安全…という概念で、プログラムを保護された環境下で動作させるものだという。(マイナビニュースより)

公式には以下のように書かれている。
macOSのApp Sandboxは、アプリケーションが意図された動作だけを行うようにする機能です。アプリケーションのサンドボックス化は、アプリケーションをあなたのMacの重要なシステムコンポーネント、あなたのデータ、そしてほかのアプリケーションから分離させます。もしアプリケーションに悪意のあるソフトウェアが含まれていたとしても、サンドボックス化が自動的にそれをブロックするので、あなたのコンピュータと情報は守られます。macOSでは、内蔵されたPDFビューアや、Adobe Flash Player、Silverlight、QuickTime、Oracle Javaなどのプラグインをサンドボックス化することで、Safariにサンドボックス保護機能を持たせています。さらにmacOSは、メール、メッセージ、FaceTime、カレンダー、連絡先、写真、メモ、リマインダー、Photo Booth、クイックルックプレビュー、Game Center、辞書、Font Book、Mac App Storeなどのアプリケーションもサンドボックス化します。

コード(クロージャ内で保存する場合)

勉強のためにいろいろ余計なプロパティも設定してみたが、こんな感じ。
savePanelの表示と一緒に、保存処理するクロージャを設定する場合。

//画像保存
@IBAction func saveImage(_ sender: Any) {
    let saveImage = myImageView.image
    
    let savePanel = NSSavePanel()
    savePanel.title = "画像保存だってよ" //表示されない?
    savePanel.prompt = "保存" //デフォルトボタンのテキスト
    savePanel.nameFieldLabel = "名前:" //ファイル名入力欄の左に表示されるテキスト
    savePanel.nameFieldStringValue = "名称未設定" //デフォルトファイル名
    savePanel.message = "画像を保存します" //ウィンドウ上部に表示される
    savePanel.canCreateDirectories = true //フォルダ新規作成ボタン表示
    savePanel.showsHiddenFiles = false //隠しファイルの表示
    savePanel.showsTagField = true //タグ付のフィールド表示有無
    savePanel.tagNames = ["画像","旅行"] //デフォルト表示するタグ
    savePanel.isExtensionHidden = true //「拡張子を隠す」ボタン。Finder設定で表示になってると機能しない
    let downloadPath = NSHomeDirectory() + "/Downloads" //Download[s]なので注意
    savePanel.directoryURL = URL(fileURLWithPath: downloadPath) //デフォルト表示のフォルダ
    savePanel.canSelectHiddenExtension = false //falseだとウィンドウ中のファイルが薄く表示される。選択はできる
    savePanel.allowedFileTypes = ["jpg","gif","png"] //許可するファイルタイプ(先頭の拡張子がデフォルトファイル名に付く)
    savePanel.allowsOtherFileTypes = false //許可されたファイルタイプ以外の許可(デフォルト不許可)
    savePanel.treatsFilePackagesAsDirectoriesfalse //アプリみたいなファイルパッケージの中までディレクトリとして表示するか否か
    
    savePanel.begin { (result) in
        if result == NSApplication.ModalResponse.OK {
            guard let tiffData = saveImage?.tiffRepresentation, //画像をTIFFのDataにする
                let imageRep = NSBitmapImageRep(data: tiffData), //bitmapデータを描画?
                let imageData = imageRep.representation(using: .jpeg, properties: [.compressionFactor : NSNumber(floatLiteral: 0.7)]) //JPEGに変換
                else {
                    print("画像変換エラー")
                    return
            }
            //画像保存処理
            do {
                try imageData.write(to: savePanel.url!)
            } catch {
                print("保存エラー \(error)")
            }
        }
    }
}

コード(delegateメソッド内で保存処理する場合)

savePanelは表示だけさせて、「保存」ボタンが押されたらdelegateメソッドで処理する場合。

//画像保存
@IBAction func saveImage(_ sender: Any) {
    let saveImage = myImageView.image
    
    let savePanel = NSSavePanel()
    savePanel.title = "画像保存だってよ" //表示されない?
    savePanel.prompt = "保存" //デフォルトボタンのテキスト
    savePanel.nameFieldLabel = "名前:" //ファイル名入力欄の左に表示されるテキスト
    savePanel.nameFieldStringValue = "名称未設定" //デフォルトファイル名
    savePanel.message = "画像を保存します" //ウィンドウ上部に表示される
    savePanel.canCreateDirectories = true //フォルダ新規作成ボタン表示
    savePanel.showsHiddenFiles = false //隠しファイルの表示
    savePanel.delegate = self
    savePanel.showsTagField = true //タグ付のフィールド表示有無
    savePanel.tagNames = ["画像","旅行"] //デフォルト表示するタグ
    savePanel.isExtensionHidden = true //「拡張子を隠す」ボタン。Finder設定で表示になってると機能しない
    let downloadPath = NSHomeDirectory() + "/Downloads" //Download[s]なので注意
    savePanel.directoryURL = URL(fileURLWithPath: downloadPath) //デフォルト表示のフォルダ
    savePanel.canSelectHiddenExtension = false //falseだとウィンドウ中のファイルが薄く表示される。選択はできる
    savePanel.allowedFileTypes = ["jpg","gif","png"] //許可するファイルタイプ(先頭の拡張子がデフォルトファイル名に付く)
    savePanel.allowsOtherFileTypes = false //許可されたファイルタイプ以外の許可(デフォルト不許可)
    savePanel.treatsFilePackagesAsDirectoriesfalse //アプリみたいなファイルパッケージの中までディレクトリとして表示するか否か
    
    savePanel.runModal() //savePanelの表示
}

//savePanelで保存ボタンを押した際に呼ばれるDelegateメソッド
func panel(_ sender: Any, validate url: URL) throws {
    let saveImage = myImageView.image

    guard let data = saveImage?.tiffRepresentation,
        let imageRep = NSBitmapImageRep(data: data),
        let imageData = imageRep.representation(using: .jpeg, properties: [.compressionFactor : NSNumber(floatLiteral: 0.7)])
        else {
            print("なんかエラーっす")
            return
    }
    do {
        try imageData.write(to: url)
    } catch {
        print("保存エラー \(error)")
    }
}
プロパティはだいたいこんな感じ

問題点

savePanelのローカライズが不完全。プロパティで設定したところしか日本語化されない。
savePanelの下の方に保存するファイル形式(jpeg, gif, pngとか)を選ぶポップアップメニューを表示したいけど、やり方がわからん。

2018年3月31日土曜日

Xcode9.3リリース

以下、AppStoreのリリースノートを翻訳

Xcode 9.3には、iOS 11.3、watchOS 4.3、tvOS 11.3、およびmacOS High Sierra 10.13.4用のSwift 4.1およびSDKが含まれています
  • Xcode 9.3には、iOS 11.3、watchOS 4.3、tvOS 11.3、およびmacOS High Sierra 10.13.4用のSwift 4.1およびSDKが含まれています
  • [オーガナイザ]ウィンドウの[新しいエネルギー]タブには、アプリの消費電力が大きすぎるときに生成されるログが含まれます
  • 迅速なビルドタスクと他のコマンドは、ビルドのパフォーマンスを改善するために、より頻繁に並行して実行されます
  • Swiftコンパイラが-Osizeビルド設定で有効になった新しいコードサイズの最適化を追加
  • コマンドキーを押しながらシンボルをクリックすると、発信者にすばやくアクセスできます
  • アセットカタログは、ARKitアプリケーションによって現実世界で検出できるAR参照画像ファイルをサポートします
  • 新しいxccovコマンドラインツールを使用すると、コードカバレッジレポートを検査できます
  • 非常に大きなファイルで作業するときのソースエディタのパフォーマンスの向上
  • 追加のバグ修正と安定性の向上

2018年3月21日水曜日

ScrollViewのContentSize

引っかかって悩んだところ。

ContentSize

上のように、縦横240pixelのScrollViewの上に、300*200のViewを置き、その上にいくつかLabelを置いた。
そのまま実行させると横だけにスクロールして、隠れている横60pixelも見られる。(ScrollViewに直接置いた「WH240のScrollView」というLabelはスクロールしない)

ContentSizeの値を変えてみる

ContentSizeはスクロールさせる中身の大きさということなので、これをviewDidLoad()内で以下のようにそれぞれ変更した場合はどうか?
myScrollView.contentSize = CGSize(width: 250, height: 200)
myScrollView.contentSize = CGSize(width: 500, height: 200)
なぜか何の変化もなく、ちゃんと隠れた横60pixelが表示されたところでスクロールは止まる。
予想では途中までしかスクロールしなかったり、View全体が表示されてもさらにスクロールし続けると思ってたのだが、そうならない。
viewDidAppear()内で設定したところ、期待どおりの動作になった。
これで実際に置かれた中身のコンテンツの大きさに関わらず、スクロール量を調整できるわけだ。

実はコンテンツの一部だけをスクロールさせたかったのに、rootのViewの大きさ分(=画面全体)がスクロールしてしまい、困ってたのだ。

2018年3月8日木曜日

画像の作り直しでJPEGがPNGになっちゃった

Retinaディスプレイの画面をキャプチャし、それをツイッターに投稿するアプリを作ってるが、画像容量がでかくなるので縮小したい。

画面キャプチャだけ

iPhoneだとRetina解像度でちょうどいいのだが、iPadだとファイルサイズが大きくなるためツイートをはねられることがしばしばある。5MB制限らしいので、それをオーバーしてしまうようだ。
うまく投稿できても15秒くらいかかって遅い。投稿できた画像は元と同じくJPEG。

UIGraphicsBeginImageContextWithOptions(realSize, false, 2.0) //Contextを作成(Retina対応で2.0)
let ctx = UIGraphicsGetCurrentContext() //現在のContextを得る
imageView.layer.render(in: UIGraphicsGetCurrentContext()!) //Context部分に表示されている画像その他をレンダリング(ここが画面キャプチャ)
var image = UIGraphicsGetImageFromCurrentImageContext() //UIImageを作る

UIGraphicsEndImageContext()

縦横ピクセルを制限

iPadのRetina解像度のまま投稿すると縦横の長さも大きすぎるので、はみ出してる画像は最大1000ピクセルに縮小させるようコードを書いた。
いっぺんに縮小するのでなく、一度キャプチャして出来た画像を再度GraphicContextを使ってリサイズしている。
Retina解像度でキャプチャしているので、image.sizeで得られるwidth、heightは1/2になっているので注意。最終的に得られる画像はその倍になるのでややこしい。
1000pxの画像を得たいときに500pxに縮小すると最終的に1000pxになるというもの。

//画像縦横サイズ制限
//maxPixelSizeは例えば500とか入ってる
//縦横の最大を1000pxに抑えたいが、2倍精細なRetina解像度なので、image.sizeで得られる解像度はその半分になるため
var newSize = image?.size
if image!.size.width > maxPixelSize || image!.size.height > maxPixelSize {
    if image!.size.width >= image!.size.height {
        let rate = maxPixelSize / image!.size.width //縦横比
        newSize = CGSize(width: maxPixelSize, height: image!.size.height * rate)
    } else {
        let rate = maxPixelSize / image!.size.height //縦横比
        newSize = CGSize(width: image!.size.width * rate, height: maxPixelSize)
    }
        //UIImage作り直し開始。なぜかPNGになってしまう
    UIGraphicsBeginImageContext(newSize!)
    image!.draw(in: CGRect(x: 0, y: 0, width: newSize!.width, height: newSize!.height))
    image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    ⭐️
}

そうしたらなぜか画像がPNGになってしまった。縮小してるから投稿できないことはないが、1MBオーバーで縮小したありがたみが薄い。

キャプチャ時の解像度を非Retinaにするとサイズ制限しなくてもJPEGになってくれるけど、今度はiPhoneでのキャプチャ解像度が低いため、画像が小さすぎてしまう。

しょうがないので、一度JPEGのData型に直し、さらにそれをUIImageに戻して解決した。

上記コードの⭐️に挿入
//0.7は圧縮率
let imageData = UIImageJPEGRepresentation(image!, 0.7)
image = UIImage(data: imageData!)

  1. Retina解像度でContextを作ってキャプチャ
  2. UIImageとして変数に
  3. 縦横を同比率で縮小するためにwidthとheightを計算
  4. 計算したwidth、heightでContextを作り、2.のUIImageをdrawすることで画像縮小(このままだとPNGになる)
  5. UIImageをJPEG化してDataに直す(UIImageのままフォーマット変換できないから)
  6. DataをUIImageに戻して終了

原因はわからないが、UIImageって内部的にJPEGとかPNGとかの情報も持ってるのね?

2018年3月7日水曜日

文字列の長さを調べる

結論

先に言うと、文字列.count で得られる。

詳細

文字列の長さ(文字数)を調べるのに、Swiftでは多くのプログラミング言語にあるlengthなどのプロパティがない。
これはUnicodeの導入で1文字あたりのバイト数のバリエーションが増えたため、一つのlengthで対応できないためのようだ。

そこで以下のようなそれぞれに対応したcountプロパティで得ることになる。
unicodeScalarsはUTF32用だ。

"猫".utf8.count ->3
"🐱".utf8.count ->4

"猫".utf16.count ->1
"🐱".utf16.count -> 2

"猫".unicodeScalars.count ->1
"🐱".unicodeScalars.count ->1

"猫".characters.count ->1

"🐱".characters.count ->1

最後のcharactersが「見た目に何文字か」を返してくれて一番良かったのだが、Swift 4でdeprecatedになり、結局
"猫".count ->1
"🐱".count ->1
で落ち着いたようだ。
これなら楽でいいや。