トップ 追記

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|

2020-01-13 [macOS][Kotlin]null安全

Cocoa + Objective-C では、nilは許容されるものでnilに対してメソッド呼び出しを行なってもアボートしないが、Javaだとnullアクセスは例外が投げられてしまう。そのような考えの違いが、null安全についても、SwiftとKotlinで差となっているのかな?

JavaではC言語の基本的データ型に相当するのが基本型(プリミティブ型)で、C言語の構造体に相当するクラスはC言語のポインタ型に相当する参照型(リファレンス型)となる。そして、C言語と同様に値渡しのみとなる。

Kotlinでは全ての型はオブジェクトで、それは参照型(リファレンス型)となる。参照型だとJavaではnullを代入できるが、Kotlinではnullが代入できるnull許容(nullable)と、null非許容(non-nullable)がある。

null許容は型名に?を付ける。

var a: String? = null

null非許容は型名に?を付けない。以下はコンパイルでエラーとなる。

var a: String = null

null許容の変数は、nullの可能性があるので、そのまま利用するとコンパイルでエラーとなる。

fun main(args: Array) {
    var s: String? = "demo"
    var n = s.length
    print(n)
}
Error:(3, 14) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

セーフコール演算子(?.)を使うと、nullでない場合は評価され、nullだと回避される。

var s: String? = "demo"
var n = s?.length
print(n)

セーフコール演算子を使う場合は、nullでないことの確認を行うことになると思うが、素直に書くと以下のようになる。

var s: String? = "demo"
if (s != null) {
    var n = s?.length
    print(n)
}

もっとスマートに書くために、セーフコール演算子と一緒に使えるlet関数が用意されている。

var s: String? = null
s?.let {
    var n = it.length
    print(n)
}

デバッグなので強制的にnullでないものとして使うための非null表明演算子(non-null assertion operator)、または、二重感嘆符演算子(double-bang operator)と呼ばれる演算子(!!.)がある。

var s: String? = "demo"
var n = s!!.length
print(n)

別のnullチェックのやり方として、null合体演算子(null coalescing operator)、または、エルヴィス演算子と呼ばれる演算子(?:)がある。

var s: String? = null
var t = s ?: ""
var n = t!!.length
print(n)

let関数とnull合体演算子を組み合わせると、以下のように書ける。

var s: String? = null
s?.let {
    var n = it.length
    print(n)
} ?: print("s is null.")

Kotlinで特徴的なのは、文法的にnullチェックされているのが分かっている場合は、コンパイラをnullチェック済みとするところだ。

var s: String? = "demo"
if (s == null) {
    print("s is null")
    return
}
var n = s.length
print(n)

これは開発環境を提供している会社が作ったプログラミング言語だからなせる技か。Kotlinには似たような割り切りを感じる部分が他にもある。機会があれば後で取りあげたい。


2020-01-02 [macOS][Kotlin]結果とエラー情報を持つデータ

Javaでは結果を関数の戻る値で、エラー情報は例外で、だったが、Androidは例外を勧めていないことは以前から感じていた。Kotlinでは、例外は復旧できない致命的な状況で利用とし、既存のJavaモジュールが投げてくる例外を包み込みResult型があったりしている。

自分でKotlinでプログラミングする際、あえて例外を利用する必要はないと思うので、どんなやり方が合うのか調べてみて辿り着いたコードを紹介する。参考にしたのは、ここ

AndroidはActivityやFragmentが再生成されるということから、ActivityやFragmentのメンバー変数でModelを持たないだとか、再生成されてもModelが破棄されない。ModelはActivityやFragmentをメンバー変数で持っていると再生されると破棄されたものにアクセスしてしまう問題がある。それの解決策としてJetpackのViewModelやLiveDataがあるのだが、LiveDataではエラーを例外で渡せない。結果とエラー情報がLiveDataとなっているのが扱いやすいということがあるので、結果とエラー情報を持つ型を用意することにした。

sealed class FindUserResult {
    data class Found(val user: User) : FindUserResult()
    data class NotFound(val name: String) : FindUserResult()
}

Swiftだとenumを利用すればだが、Kotlinのenum classは状態を定数で持つだけ。その代わり、sealed classを使えば値を持てる。そして、サブクラスを用意することによって、状態を持てる。

上の例では、FindUserResultのサブクラスがFoundとNotFoundになっていて 、プライマリコンストラクタでそれぞれのプロパティのuserとnameを宣言している。

この型を戻り値にした関数がこれ。

fun findUserByName(name: String): FindUserResult {
    ....
    if (見つかった) return FindUserResult.Found(user)
    else          return FindUserResult.NotFound(name)
}

成功したらFoundを返している。失敗の場合はNotFoundを返している。処理の結果や、エラー情報はコンストラクタのパラメータで設定している。

この戻り値を受け取った側のコードがこれ。

val result = findUserByName("bitz")
when (result) {
    is FindUserResult.Found -> println("find ${result.user}")
    is FindUserResult.NotFound ->println("find ${result.name}")
}

型で成功か失敗が分かり、プロパティで結果やエラー情報が取れる。


2020-01-01 [macOS][Kotlin]開発環境を用意する

仕事でAndroidアプリケーションをKotlinで開発しているので、macOSでKotlinを試してみる。

KotlinといえばAndroidStudioでAndroidアプリケーションをプログラミングだと思うが、macOS上で動作するKotlinで書かれたプログラムを動かしたいので、開発環境としてIntelliJ IDEAを使うことにする。IntelliJ IDEAはUltimate EditionとCommunity Editionがあるか、Kotlinプログラミングの学ぶ目的なら、Community Editionで大丈夫だ。JetBrains社のWebサイト (https://www.jetbrains.com/ja-jp/idea/download/#section=mac) からダウンロードしよう。

download

IntelliJ IDEAが入手できたら、Java仮想マシンで動作するプログラムのプロジェクトを生成する。

新規プロジェクト
Kotlin_JVM
Project name

この状態では、Kotlinのソースファイルは作られていないので、srcフォルダを右クリックして、New > Kotlin File/Class を選択して、Kotlinファイルを生成する。

new file
Kotlin File

そして、hello, worldを印字するコードを書く。

fun main(args: Array) {
    println("hello, world")
}

Run.

run

hello, world と印字されている。


2019-12-24 [Cocoa][Swift]Cocoa.swiftのご案内

2019年も、もうすぐ終わります。今年一年ありがとうございました。
勉強会って参加するのもいいですが、運営するのも色々得るものがあるというのも再認識できた一年でした。

以下が、今年開催された勉強会の一覧です。

  • Cocoa.swift 2019-01 (macOS/iOSアプリケーション開発勉強会)
    • 第116回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年1月16日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/111294/
  • Cocoa.swift 2019-02 (macOS/iOSアプリケーション開発勉強会)
    • 第117回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年2月20日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/117577/
  • Cocoa.swift 2019-03 (macOS/iOSアプリケーション開発勉強会)
    • 第118回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年3月13日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/121658/
  • Cocoa.swift 2019-04 (macOS/iOSアプリケーション開発勉強会)
    • 第119回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年4月24日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/124467/
  • Cocoa.swift 2019-06 (macOS/iOSアプリケーション開発勉強会)
    • 第120回 Cocoa勉強会 関東 / MOSAオフラインミーティング
    • 2019年6月12日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/129470/
  • Cocoa.swift 2019-07 (macOS/iOSアプリケーション開発勉強会)
    • 第121回 Cocoa勉強会 関東
    • 2019年7月17日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/135361/
  • Cocoa.swift 2019-09 (macOS/iOSアプリケーション開発勉強会)
    • 第122回 Cocoa勉強会 関東
    • 2019年9月11日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/145138/
  • Cocoa.swift 2019-10 (macOS/iOSアプリケーション開発勉強会)
    • 第123回 Cocoa勉強会 関東
    • 2019年10月30日
    • 池袋コワーキングスペース OpenOffice FOREST
    • https://cocoa-kanto.connpass.com/event/147201/

昨年度と比較して開催時期がほぼ同じと安定していとうか進歩がないというか。


2019-11-30 [Cocoa][Swift]XCFramework

Darwinで採用されています実行形式のバイナリ・フォーマットMach-oは、一つのファイルに複数のアーキテクチャのバイナリが格納できるという素晴らしい特徴があるのですが、同じCPUで異なるシステム向けのバイナリは同時に格納できないという欠点があるようです。以前だと、これで問題はなかったのですが、例えば、iPad OS向けアプリのソースからmacOSアプリを作ることができるUIKit for Mac (Catalyst)だと、x86_64でiOSとiPhoneシミュレータ(macOS)という場合が発生して、同一ファイルに格納できないという問題が発生します。

おそらく、これの対策として用意されたのが、Xcode 11から利用できるXCFramework。簡単に説明すると複数のフレームワークを一つにできるというものだ。

具体的には、MoltenGLというライブラリはiOSとmacOS向けのフレームワークが用意されていて、これを以下のコマンドでXCFrameworkにまとめられる。

% xcodebuild -create-xcframework \
> -framework MoltenGL-0.25.0/MoltenGL/iOS/framework/MoltenGL.framework \
> -framework MoltenGL-0.25.0/MoltenGL/macOS/framework/MoltenGL.framework \
> -output MoltenGL.xcframework
xcframework successfully written out to: MoltenGL.xcframework

この中身をtreeコマンドで確認してみる。

% tree MoltenGL.xcframework
MoltenGL.xcframework
├── Info.plist
├── ios-armv7_arm64
│   └── MoltenGL.framework
│       ├── Headers
│       │   ├── MoltenGL.h
│       │   ├── mglDataTypes.h
│       │   ├── mglEnv.h
│       │   ├── mglGLKitDataTypes.h
│       │   ├── mglMetalState.h
│       │   ├── mglext.h
│       │   └── mln_env.h
│       └── MoltenGL
└── macos-x86_64
    └── MoltenGL.framework
        ├── Headers -> Versions/Current/Headers
        ├── MoltenGL -> Versions/Current/MoltenGL
        └── Versions
            ├── A
            │   ├── Headers
            │   │   ├── MoltenGL.h
            │   │   ├── mglDataTypes.h
            │   │   ├── mglEnv.h
            │   │   ├── mglGLKitDataTypes.h
            │   │   ├── mglMetalState.h
            │   │   ├── mglext.h
            │   │   └── mln_env.h
            │   └── MoltenGL
            └── Current -> A
 
10 directories, 18 files

単なるx86_64でなく、macOSのx86_64となっている。


2019-10-31 [macOS]zshでgitのブランチ名を表示させる

Mojaveまでは、手動でCommand Line Toolsをインストールしたら設置されるスクリプトを使ってbashでgitのブランチ名を表示させていたが、Catalinaからは設置されないようになったようだ。また、Catalinaからはzshがデフォルト・シェルになったということで、zshでgitのブランチ名を表示させる方法を調べた。

ホームディレクトリ配下に.zshというディレクトリを作って、そこにgit-completion.zshとgit-prompt.shをダウンロードして配置する。

% cd
% mkdir .zsh
% curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.zsh -o ~/.zsh/git-completion.zsh
% curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh -o ~/.zsh/git-prompt.sh

.zshrcに以下のコードを追加する。

# Git
 
fpath=(~/.zsh $fpath)
 
if [ -f ${HOME}/.zsh/git-completion.zsh ]; then
        zstyle ':completion:*:*:git:*' script ~/.zsh/git-completion.zsh
fi
 
if [ -f ${HOME}/.zsh/git-prompt.sh ]; then
        source ${HOME}/.zsh/git-prompt.sh
fi
 
GIT_PS1_SHOWDIRTYSTATE=true
GIT_PS1_SHOWUNTRACKEDFILES=true
GIT_PS1_SHOWSTASHSTATE=true
GIT_PS1_SHOWUPSTREAM=auto
 
setopt PROMPT_SUBST ; PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '

これで、zshのプロンプトにgitのブランチ名が表示されるようになる。


2019-10-10 [macOS][Catalina][zsh]bashからzshへ移行する

macOS Catalinaのデフォルト・シェルはzshに変わった。でも、以前のバージョンからのバージョンアップに、自動でzshに変わることはない。勝手に変更されると困るので当然だが。

自分は新しいもの好きなので(zshはとても古いが)、zshに移行してみた!

Catalinaにバージョンアップ後、以下のコマンドでzshのパスを確認する。

$ cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.
# /bin/false was added for FTP users that do not have a home directory.
 
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/bin/false

以下のコマンドを実行して、ターミナルを再起動する。

$ chsh -s /bin/zsh

次は、設定ファイルの準備だと思う。csh系とBourne Shell系が混じっている。

.zprofile
.zshrc
.zlogin
.zlogout

以前のシェルがBashの場合、極端な話、Bourne Shellと互換があるBashから、Bourne Shellと互換があるZshへの移行なので、設定に基本的なBourne Shellの記述しかしていない場合、以下のようにコピー後に、多少の手直しで、大丈夫。

% cp .bash_profile .zprofile
% cp .bashrc .zshrc

2019-09-11 [cocoa][swift][kotlin]Cocoa.swift 2019-09に行ってきた

会場は池袋コワーキングスペース OpenOffice FOREST、サンシャイン側だ。

_ 発表

「NSTextViewにコマンドパレットをつける」キーボードのみで操作できるように、NSTextViewにコマンドパレットをつける仕組みの発表で、CMD + Lで起動し、出現したNSTextFieldに文字を打つと、関連するメニューやコンテンツが選択できるようになるものだ。

「macOS/iOS/Android Tips枠(その1)」iOS/Androidの新OSの情報や、ストア申請などについて発表された。

「Tweeting(最近のTweet機能の実装方法)」システムやTwitter社からネイティブ実装のためのAPIが提供されなくなった現在にオススメできるTweet機能のiOSとAndroidの実装方法が紹介された。

「macOS/iOS/Android Tips枠(その2)」先日開催されたiOSDC Japanで発表者が興味を持ったものが紹介された。


2019-08-09 [cocoa][swift][Kotlin]Tweeting

システム側でのSNS共有のサポートが終了したり、公式のTwitter Kit SDKのサポートが停止するなどで、スマートフォン・アプリケーションにTweet機能を組み込む方法が変わってきているので、今時点のTweet機能を組み込む方法を調べてみた。

  • ios
    • Social.framework
      iOS11から廃止。
    • Twitter Kit SDK
      2018年10月末でサポート終了。
  • Android
    • Twitter Kit SDK
      2018年10月末でサポート終了。

方向としては、ネイティブ・コード向けライブラリの提供はやめて、Web技術を利用して欲しいということのようだ。

_ iOS

Universal Linksを利用した方法。Twitterアプリケーションがインストールされていない場合はWebブラウザで、インストールされている場合は、Twitterアプリケーションでの投稿となる。

@IBAction func intentTweet(_ sender : Any) {
    let text = "Web Intentの例"
    let encodedText = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
    if let encodedText = encodedText,
        let url = URL(string: "https://twitter.com/intent/tweet?text=\(encodedText)") {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
}

具体的な仕様は以下で説明されている。

このURLに文言をパラメータとして設定してオープンするという仕組みだ。

Twitterアプリケーションがインストールされていない

Twitterアプリケーションがインストールされていない

Twitterアプリケーションがインストールされている

Twitterアプリケーションがインストールされている

iOSの共有機能を提供するUIActivityViewControllerを利用する方法。Twitterアプリがインストールされていないと候補に現れないだとか、SNS共有っぽくはない。でも、テキストに加え、画像も直に共有できる。

@IBAction func activityTweet(_ sender : Any) {
    let text = "共有機能を利用する"
    let bundlePath = Bundle.main.path(forResource: "brownout", ofType: "jpg")
    let image = UIImage(contentsOfFile: bundlePath!)
    let shareItems = [image, text] as [Any]
    let controller = UIActivityViewController(activityItems: shareItems, applicationActivities: nil)
    present(controller, animated: true, completion: nil)
}

共有先を選ぶ

共有機能

Twitterを選ぶと、Twitterアプリの投稿画面。画像も扱える。

共有機能 Twitter

_ Android

ボタンと配置する。最近のAndroid Studioは向上している。Xcode並みに開発しやすくなっている。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <Button
        android:id="@+id/intentTweetButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="intent tweet"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <Button
        android:id="@+id/shareCompatButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:text="ShareCompat"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/intentTweetButton" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

インテントを利用する方法。TwitterアプリがインストールされていないとWebブラウザで、インストールされているとTwitterアプリが利用される。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
 
    val intentTweetButton: Button = findViewById(R.id.intentTweetButton)
    intentTweetButton.setOnClickListener {
        shareTwitter()
    }
}
 
fun shareTwitter() {
    val message = "shareTwitter intent tweet"
    try {
        val sharingIntent = Intent(Intent.ACTION_SEND)
        sharingIntent.setClassName("com.twitter.android", "com.twitter.android.PostActivity")
        sharingIntent.putExtra(Intent.EXTRA_TEXT, message)
        startActivity(sharingIntent)
    }
    catch (e: Exception) {
        Log.e("In Exception", "Comes here")
        val i = Intent()
        i.putExtra(Intent.EXTRA_TEXT, message)
        i.action = Intent.ACTION_VIEW
        i.data = Uri.parse("https://mobile.twitter.com/compose/tweet")
        startActivity(i)
    }
}

iOSのWeb Intentと同様な仕組みだ。

intent tweet

共有機能を利用する方法。TwitterアプリがインストールされていないとTweetできない。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
 
    val shareCompatButton: Button = findViewById(R.id.shareCompatButton)
    shareCompatButton.setOnClickListener {
        shareCompat()
    }
}
 
fun shareCompat() {
    val message = "shareCompat"
    val builder = ShareCompat.IntentBuilder.from(this) 
    builder.setChooserTitle("Choose App")
    builder.setText(message)
    builder.setType("text/plain")
    builder.startChooser()
}

共有先を選ぶ

shareCompat

Twitterを選ぶと、Twitterアプリの投稿画面。画像も扱えるはずだが、自分はうまくいかなかった。

shareCompat tweet

2019-07-20 [cocoa][swift]NSUndoManager

NSUndoManagerの利用は、Swiftで楽になったと思うが、その仕組みが見えにくくなったと思うので、Objective-Cの場合から説明する。

CocoaのUndoとRedoは、NSInvocationというクラスでNSObjectの子クラスとメソッドを保持し、それをNSUndoManager内のスタックで管理することで実現している。

なんらかの操作を行うと、Undoに必要なNSInvocationのインスタンスがUndoスタックに積まれていく。

undo stack

ユーザがUndoを行うと、Redoに必要なNSInvocationのインスタンスがRedoスタックに積まれていく。

redo stack

Objective-Cのコードで、以下のようになる。

- (void)makeItHotter
{
    temperature = temperature + 10;
    [[undoManager prepareWithInvocationTarget:self] makeItColder];
}
 
- (void)makeItColder
{
    temperature = temperature - 10;
    [[undoManager prepareWithInvocationTarget:self] makeItHotter];
}

makeItHotterで温度を10度上げて、makeItColderをNSUndoManagerに積む。

makeItColderでは、温度を10度下げて、makeItHotterをNSUndoManagerに積む。

これをSwiftを書く場合、積むメソッドのselectorを用意するのが面倒になるのだが、selectorを必要としないメソッドが用意されていた。以下のようになる。

func makeItHotter() {
    var temperature = self.textField.intValue
    temperature = temperature + 10
    self.undoManager?.registerUndo(withTarget: self, handler: {
        vc in
        vc.makeItColder()
    })
    self.textField.intValue = temperature
}
 
func makeItColder() {
    var temperature = self.textField.intValue
    temperature = temperature - 10
    self.undoManager?.registerUndo(withTarget: self, handler: {
        vc in
        vc.makeItHotter()
    })
    self.textField.intValue = temperature
}

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/mac/Temperature - GitHub

トップ 追記