- 投稿日:2021-03-29T18:21:29+09:00
【Swift】SlideshowAppで学んだこと
はじめに
SlideshowAppを作成しましたので、そこで学んだことの備忘録です。
GitHub
学んだこと
グラデーションの扱いです。
let width = self.view.frame.size.width let height = self.view.frame.size.height let gradientLayer = CAGradientLayer() gradientLayer.frame = CGRect(x: 0, y: 0, width: width, height: height) gradientLayer.colors = [UIColor(red: 0.8, green: 0.0, blue: 0.73, alpha: 1).cgColor, UIColor(red: 0.1, green: 0.0, blue: 0.40, alpha: 1).cgColor] gradientLayer.startPoint = CGPoint.init(x: 0, y: 0.5) gradientLayer.endPoint = CGPoint.init(x: 1, y: 0.5) self.view.layer.insertSublayer(gradientLayer, at: 0)このようにすることで、背景色がグラデーションでいい感じになってくれます。
公式ドキュメント: CAGradientLayer
スニペットなどに登録しておくことをお勧めします!おわりに
おわりです。
- 投稿日:2021-03-29T17:57:28+09:00
【Swift】Extensionまとめ(初級)
はじめに
Extensionの基礎的な扱い方をまとめました。
Extensionとは?
Extensionキーワードを使うことで既存の型に方を構成する要素(プロパティやメソッドやイニシャライザ)を追加する事ができます。
具体例を見ていきましょう。メソッドでの利用
以下のようにすることで、
plusOne
メソッドをIntに追加できます。
self
はこのメソッドの呼び出し元(今回は5)を表しています。(詳細は後述します)extension Int { func plusOne() -> Int { return self + 1 } } print(5.plusOne()) // 6プロパティでの利用
extensionはストアドプロパティは追加できませんが、コンピューテッドプロパティを追加する事ができます。
extension Int { var minus10: Int { return self - 10 } } print(100.minus10) // 90イニシャライザでの利用
イニシャライザを追加してみます。
enum BloodType { case A case B case O case AB var title: String { return "あなたの血液型は" } var message: String { switch self { case .A: return "Aです" case .B: return "Bです" case .O: return "Oです" case .AB: return "ABです" } } } extension UIAlertController { convenience init(blood: BloodType) { self.init(title: blood.title, message: blood.message, preferredStyle: .alert) } } let blood = BloodType.AB let alert = UIAlertController(blood: blood)プロトコルでの利用
プロトコルをエクステンションすることもできます。
protocol Book { var name: String { get } var price: Int { get } } extension Book { func printName() { print(name) } func printPrice() { print(price) } func printSelf() { print(self) } func printMyType() { print(type(of: self)) } } struct MyBook: Book { var name: String var price: Int } let myBook = MyBook(name: "マイブック", price: 1000) myBook.printPrice() // 1000 myBook.printSelf() // MyBook(name: "マイブック", price: 1000) myBook.printMyType() // MyBookこれで
Book
プロトコルを採用した型はextension Book
内のメソッドを扱う事ができます。制約の追加
プロトコルエクステンションは型による制約をつけることができ、この条件を満たすもののみプロトコルエクステンションを有効にできます。
extension Collection where Element == Int { var total: Int { return self.reduce(0) { $0 + $1 } } } let intArray = [1, 2, 3, 4, 5] print(intArray.total) // 15 let stringArray = ["a", "b", "c"] print(stringArray.total) // error条件は
Collection
の要素Element
がInt
であると言う意味です。ですので、intArray
の要素はInt
なのでtotal
プロパティを扱う事ができますが、stringArray
は要素がString
なのでtotal
プロパティにアクセスしようとするとエラーになります。おわりに
エクステンションの基礎的な使い方を紹介しました。以上です。
- 投稿日:2021-03-29T17:57:28+09:00
【Swift】Extensionまとめ(基礎)
はじめに
Extensionの基礎的な扱い方をまとめました。
Extensionとは?
Extensionキーワードを使うことで既存の型に方を構成する要素(プロパティやメソッドやイニシャライザ)を追加する事ができます。
具体例を見ていきましょう。メソッドでの利用
以下のようにすることで、
plusOne
メソッドをIntに追加できます。
self
はこのメソッドの呼び出し元(今回は5)を表しています。(詳細は後述します)extension Int { func plusOne() -> Int { return self + 1 } } print(5.plusOne()) // 6プロパティでの利用
extensionはストアドプロパティは追加できませんが、コンピューテッドプロパティを追加する事ができます。
extension Int { var minus10: Int { return self - 10 } } print(100.minus10) // 90イニシャライザでの利用
イニシャライザを追加してみます。
enum BloodType { case A case B case O case AB var title: String { return "あなたの血液型は" } var message: String { switch self { case .A: return "Aです" case .B: return "Bです" case .O: return "Oです" case .AB: return "ABです" } } } extension UIAlertController { convenience init(blood: BloodType) { self.init(title: blood.title, message: blood.message, preferredStyle: .alert) } } let blood = BloodType.AB let alert = UIAlertController(blood: blood)プロトコルでの利用
プロトコルをエクステンションすることもできます。
protocol Book { var name: String { get } var price: Int { get } } extension Book { func printName() { print(name) } func printPrice() { print(price) } func printSelf() { print(self) } func printMyType() { print(type(of: self)) } } struct MyBook: Book { var name: String var price: Int } let myBook = MyBook(name: "マイブック", price: 1000) myBook.printPrice() // 1000 myBook.printSelf() // MyBook(name: "マイブック", price: 1000) myBook.printMyType() // MyBookこれで
Book
プロトコルを採用した型はextension Book
内のメソッドを扱う事ができます。制約の追加
プロトコルエクステンションは型による制約をつけることができ、この条件を満たすもののみプロトコルエクステンションを有効にできます。
extension Collection where Element == Int { var total: Int { return self.reduce(0) { $0 + $1 } } } let intArray = [1, 2, 3, 4, 5] print(intArray.total) // 15 let stringArray = ["a", "b", "c"] print(stringArray.total) // error条件は
Collection
の要素Element
がInt
であると言う意味です。ですので、intArray
の要素はInt
なのでtotal
プロパティを扱う事ができますが、stringArray
は要素がString
なのでtotal
プロパティにアクセスしようとするとエラーになります。おわりに
エクステンションの基礎的な使い方を紹介しました。以上です。
- 投稿日:2021-03-29T17:43:15+09:00
【iOS】Gmail APIでメール情報を取得
目的と手順の説明
今回は、私の最新のGmailを誰から受信したのか?を取得することを目的にするよ〜〜〜〜〜。
1.Gmail APIを有効にする
2.認証画面を作成
3.OAuth2.0を利用して、クライアントアプリにトークンを受けとる
4.Gmail APIを叩く
下の記事を読んでから、この記事を読み進めることをおすすめします。
一番分かりやすい OAuth の説明1.Gmail APIを有効にする
(もしかしたら、1より先に2をしなあかんかったかもしれん。。。)
Gmail APIを有効
アプリを公開しないなら、全てを記述する必要がないので5分ぐらいで終わります。認証情報の隣にある認証情報を作成というボタンをクリックしてOuthクライアントIDを選択する
→アプリケーションの種類をiOSにする→バンドルIDはXcodeで作成したアプリのBoundleIdentifierをコピペする。2.認証画面を作成
認証画面は、クライアントアプリがアクセストークンを作成する時に、エンドユーザーに対して認可する権限を与える画面です。この画面自体プログラマーが作る必要がありません。もうすでに用意されている。
認証画面の途中でスコープ
を聞かれると思います。そのとき、制限付きのスコープのhttps://mail.google.com/を選択する。(今回はメール情報を取得するから)
テストユーザーも追加しておくこれでサーバーサイドの設定はしゅーーーりょーーーーーーーーう!!
これから、Xcodeで作業します。3.OAuth2.0を利用して、クライアントアプリにトークンを受けとる
OAuthを利用する準備
この記事みながらセットアップした。
ライブラリのインストールにCocoaPodsを使用します。
ターミナルを開けて〜〜〜〜〜cd アプリのディレクトリアプリのディレクトリに移動します。
pod initポッドファイルを作成。
pod 'GoogleAPIClientForREST/Gmail', '~> 1.3.0' pod 'GTMAppAuth'podファイルに上記を追加して
pod updateアップデートしたら、大丈夫だぁ〜〜〜〜〜
アクセストークンをリクエスト
今回使う変数を全て定義するぞ〜
その前に、したのインポートを忘れずにimport AppAuth import GoogleAPIClientForREST import GTMAppAuthprivate let scopes = ["https://mail.google.com/"] private let kClientID = "クライアントID" private let kRedirectURL = URL.init(string: "リダイレクトURL"+":/oauthredirect")! private let configuration = GTMAppAuthFetcherAuthorization.configurationForGoogle() private var authorization: GTMAppAuthFetcherAuthorization? private let userId=Gメールアドレスscopes
2の認証画面の設定で使用したスコープをいれる。
kClientIDとkRedirectURL
google.com/api
上のリンクから認証情報のOAuth2.0クライアントIDに追加されているアプリの編集画面に行けばみれる!
クライアントID
とiOSのURLスキーム
あります。
kClientID=クライアントID
kRedirectURL=iOSのURLスキーム+":/oauthredirect"です
(:/oauthredirect"を忘れずに! 俺は忘れていた。)認証画面を表示させる
private func showAuthorizationDialog() { let request = OIDAuthorizationRequest.init(configuration: configuration, clientId: self.kClientID, scopes: scopes, redirectURL: kRedirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.currentAuthorizationFlow=OIDAuthState.authState(byPresenting: request, presenting: self, callback: { (authState, err) in if let error = err { NSLog("\(error)") } else { if let authState = authState { self.authorization = GTMAppAuthFetcherAuthorization.init(authState: authState) GTMAppAuthFetcherAuthorization.save(self.authorization!, toKeychainForName: "authorization") } } }) }軽く説明していきます。
let request = OIDAuthorizationRequest.init(configuration: configuration, clientId: self.kClientID, scopes: scopes, redirectURL: kRedirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil)認証画面を表示するためのサーバーに対するリクエストを表しています。
responseType:は応答のタイプを表しています。
additionalParameters:はクライアントの追加の許可パラメーターです。今回はないのでnilを渡します。let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegateAppDelegateにアクセスしている理由は後ほどわかります。
appDelegateに変数を置いている理由は、異なるSwiftファイル間で変数を共有するためです。
AppDelegateに宣言した変数は、どこのViewControllerでも共有できる。appDelegate.currentAuthorizationFlow=OIDAuthState.authState(byPresenting: request, presenting: self, callback: { (authState, err) in if let error = err { NSLog("\(error)") } else { //OIDAuthStateからGTMAppAuthFetcherAuthorizationを作成します。 if let authState = authState { self.authorization = GTMAppAuthFetcherAuthorization.init(authState: authState) GTMAppAuthFetcherAuthorization.save(self.authorization!, toKeychainForName: "authorization") } } })上記のコードで認証要求を実行します。
appleDelegateクラスにcurrentAuthorizationFlowをOIDExternalUserAgentSession?型で宣言しておく。
より詳しく知りたいかたは下のリンクに飛べ〜〜〜〜〜〜〜〜
README.md// Serialize to Keychain GTMAppAuthFetcherAuthorization.save(_authorization, toKeychainForName: kGTMAppAuthExampleAuthorizerKey) // Deserialize from Keychain let authorization = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: kGTMAppAuthExampleAuthorizerKey) // Remove from Keychain GTMAppAuthFetcherAuthorization.removeFromKeychain(forName: kGTMAppAuthExampleAuthorizerKey)一番上のコードでキーチェーンを利用して、ログイン状態を保存します。
いくつもログインしたい場合は、名前を変えて保存したらいいだけだから、すごい便利ですね。
したの記事でキーチェーンの理解をしておくといいかももも桃もお
キーチェーンの記事ここまでで、認証完了!!
4.Gmail APIを叩く
やっと最後の章になります。長かった〜、、、、
コード全体
@objc func message(_ senfder:UIButton){ let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: userId) let service = GTLRGmailService() service.authorizer = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: "authorization") service.executeQuery( query, delegate: self, didFinish: #selector(callback) ) } @objc func callback( ticket : GTLRServiceTicket, finishedWithObject response : GTLRGmail_ListMessagesResponse, error : NSError? ){ if let error = error { print("メッセージリストの取得に失敗しました。") print(error) return } print("メッセージリストの取得に成功しました。") guard let message=response.messages?.first else {return} guard let messageid=message.identifier else {return} guard let url=URL(string: "https://gmail.googleapis.com/gmail/v1/users/\(userId)/messages/\(messageid)") else {return} var urlRequest=URLRequest(url: url) guard let token=authorization?.authState.lastTokenResponse?.accessToken else {return} urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let session=URLSession(configuration: URLSessionConfiguration.default) let task=session.dataTask(with: urlRequest){ (data,urlResponse,err) in if let err=err { print("失敗しました\(err)") } do{ let responseJson=try JSONDecoder().decode(res.self, from: data!) responseJson.payload.headers.forEach { if($0.name=="From"){ print($0.value) } } }catch(let err){ print("失敗した\(err)") } }.resume() }コードの解説
説明ながなる〜〜〜〜〜〜〜〜疲れた〜〜〜〜〜〜〜〜
@objc func message(_ senfder:UIButton){ let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: userId) let service = GTLRGmailService() service.authorizer = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: "authorization") service.executeQuery( query, delegate: self, didFinish: #selector(callback) ) }messageメソッドはボタンが押されたら呼ばれるメソッドです。
詳しく見ていきます。let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: userId)GTLRGmailQuery_UsersMessagesList.queryはユーザーのメールボックス内のメッセージを一覧表示する命令です。useIdはGmailアドレスです。
let service = GTLRGmailService() service.authorizer = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: "authorization")GTLRGmailServiceはGmailAPIクエリを実行するための作業人みたいな感じだと思います。
この作業する人が本人かどうか調べます。authorizerに、認証完了している情報をキーチェーンを用いて、GTMAppAuthFetcherAuthorizationを生成して代入する。これは、認証許可している人かどうか調べるためです。service.executeQuery( query, delegate: self, didFinish: #selector(callback) )作業人が本人だったら作業開始します。
その時、callback関数の引数に結果が帰っていきます。
callbackの中身を具体的に見ていきます。@objc func callback( ticket : GTLRServiceTicket, finishedWithObject response : GTLRGmail_ListMessagesResponse, error : NSError? ){ if let error = error { print("メッセージリストの取得に失敗しました。") print(error) return } print("メッセージリストの取得に成功しました。") guard let message=response.messages?.first else {return} guard let messageid=message.identifier else {return} guard let url=URL(string: "https://gmail.googleapis.com/gmail/v1/users/\(userId)/messages/\(messageid)") else {return} var urlRequest=URLRequest(url: url) guard let token=authorization?.authState.lastTokenResponse?.accessToken else {return} urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let session=URLSession.shared session.dataTask(with: urlRequest){ (data,urlResponse,err) in if let err=err { print("失敗しました\(err)") } do{ let responseJson=try JSONDecoder().decode(res.self, from: data!) responseJson.payload.headers.forEach { if($0.name=="From"){ print($0.value) } } }catch(let err){ print("失敗した\(err)") } }.resume() }if let error = error { print("メッセージリストの取得に失敗しました。") print(error) return }引数のerrorに値が入っていたら、エラーを表示し、returnでcallback関数を抜けます。
guard let message=response.messages?.first else {return} guard let messageid=message.identifier else {return}response.messagesは,配列でいーーーっぱい、messageIDとthreadIDが格納されています
今回最初のメッセージが取得したいのでresponse.messages?.firstで配列の最初のメッセージ情報を取得します
ほんでもって,message.identifierでメッセージIDを取得します.このメッセージIDを用いて,URLを生成します.
guard let url=URL(string: "https://gmail.googleapis.com/gmail/v1/users/\(userId)/messages/\(messageid)") else {return} var urlRequest=URLRequest(url: url) guard let token=authorization?.authState.lastTokenResponse?.accessToken else {return} urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")先ほどのmessageidを用いてURLを生成します。
その後、URLRequest型を定義します。
URLRequest型は通信のリクエストを表現する型です。HTTPリクエストのURL、ヘッダ、メソッド、ボディなどの情報を持ちます。
ヘッダにはアクセストークンが必要です。
(urlRequest.httpMethod="GET"を途中に書いても問題ないと思います。)
ヘッダにアクセストークンを追加します。
forHTTPHeaderFieldについては、通信に関するお話でよくわからないです。( i _ i )
また、勉強しておきます。
Appleのドキュメントに詳しく乗っています。let session=URLSession.shared session.dataTask(with: urlRequest){(data,urlResponse,err) in if let err=err { print("失敗しました\(err)") } do{ let responseJson=try JSONDecoder().decode(res.self, from: data!) responseJson.payload.headers.forEach { if($0.name=="From"){ print($0.value) } } }catch(let err){ print("失敗した\(err)") } }.resume()URLSessionクラスは,URL経由でのデータの送受信に使います。
リクエストはタスクと呼ばれます。
詳しく知りたい方はしたの記事を参考に
【Swift】URLSessionまとめ
URLSession.sharedはURLSessionクラスにデフォルト値が設定されたインスタンスです。if let err=err { print("失敗しました\(err)") }通信が完了して、エラーが帰ってきたらエラーを表示します
do{ let responseJson=try JSONDecoder().decode(res.self, from: data!) responseJson.payload.headers.forEach { if($0.name=="From"){ print($0.value) } } }catch(let err){ print("失敗した\(err)") }通信が完了してdataの中に一番最新のメール情報に関するJSONファイルがあるのでパースします。
JsonDecoder().decode()でSwiftの型に変換しています。
今回、誰から連絡きたのかが知りたいのでforEachでループして探しています。。。
resは以下のようです。struct res: Codable{ var payload:Playload struct Playload:Codable { var headers:[Header] } struct Header:Codable { var name:String var value:String } }誰から、連絡きていたのか?を記事に載せようとしたけど、しょーもない人だったのでやめときます。。
感想
初心者なのでわからないことばかりですが、少しでもお力になれれば幸いです。
- 投稿日:2021-03-29T17:14:16+09:00
AWS Amplifyの認証情報を他のAWSサービスのSDKで流用した[Swift Storyboard]
AWS Amplifyを使うとモバイルアプリで簡単に認証機能を搭載できますが、Amplifyの対応していないAWSサービスを使わなければならない場面があったのでメモ。
AWS歴半年の初学者の備忘録ですので、誤り等あればご指摘いただけると幸いです。前提
- Amplify CLIでプロジェクトに認証機能を追加している(参考:AWS AmplifyによるiOSアプリ開発入門(Part1))
- 対応するIDプールのIAMロールに必要なポリシーをアタッチしている
具体的には amplify-xxxx-yyyy-zzzz-authRole-idp のようなロール名です実装
認証が必要なSDKの直前で以下を追記
let credentialsProvider = AWSMobileClient.default().getCredentialsProvider() let configuration = AWSServiceConfiguration(region:.APNortheast1, credentialsProvider:credentialsProvider) AWSServiceManager.default().defaultServiceConfiguration = configuration動かないときは
- IDプールに対応するIAMロールで実行したいアクションが許可されているかを確認する。
AWSMobileClient.sharedInstance()~
としている箇所があればAWSMobileClient.default()~
と変更する。
前者のメソッドは今後は後者に移行されていくようです。
- 投稿日:2021-03-29T13:58:32+09:00
【Swift】TargetedExtensionとは
はじめに
僕のSwiftで最も好きな文法はExtensionです笑
基礎的なExtensionの使い方はこちらをみてみてください。
TargetedExtensionというものがあるらしいので、みていきましょう。Extensionとは?
詳細なエクステンションの説明は省きますが、エクステンションは以下のように構造体やクラス、プロトコルを拡張してメソッドなどを追加できます。
例を見てみましょう。extension Int { var minus10: Self { return self - 10 } func plus100() -> Self { return self + 100 } } print(200.minus10) //190 print(200.plus100().plus100().minus10) //390こちらも合わせてご覧ください(メソッドチェーン)
TargetedExtensionとは?
前項の例だといいのですが、命名によってはこのメソッドは既にあるものなのか、自分で作ったものなのかわからない場合がありますし、Xcodeの予測変換でメソッドやプロパティが自分で作った数だけ多くなるので、見辛くなってしまうかもしれません。できればXcodeの補完を汚さずに、自作のメソッドなどにアクセスしたいです。
このような時に役に立つのがTargetedExtensionです。
前項のプログラムを修正していきましょう。protocol SomeComapatible { associatedtype ComapatibleType var own: ComapatibleType { get } } class Some<T> { private let base: T init(_ base: T) { self.base = base } } extension SomeComapatible { var own: Some<Self> { return Some(self) } } extension Int: SomeComapatible { } extension Some where T == Int { var minus10: Int { return base - 10 } func plus100() -> Int { return base + 100 } }こうすることで、以下のように
own
を間に挟まないとminus10
プロパティやplus100
メソッドにアクセスできないようにできました。print(1000.minus10) //エラー print(1000.own.plus100()) //1100解説
ステップ1
このプロトコルを採用したものは
own
プロパティを持つようにする。protocol SomeComapatible { associatedtype ComapatibleType var own: ComapatibleType { get } }ステップ2
Int
に先ほどのプロパティを準拠させます。しかし、own
プロパティの実装がありませんので、次で実装しましょう。extension Int: SomeComapatible { }ステップ3
以下のように実装します。
Some<Self>
のSelf
はプロトコルSomeComapatible
を準拠させた具体的な型なので、今回はInt
ですね。
そして、return Some(self)
のself
はprint(1000.own.plus100())
このように書いた時の1000
のことです。
このように、ジェネリクスも用いてあげることで、Int
だけでなくString
などでも同じown
を利用する事ができます。class Some<T> { private let base: T init(_ base: T) { self.base = base } } extension SomeComapatible { var own: Some<Self> { return Some(self) } }ステップ4
では、具体的にメソッドなどを定義していきます。
extension Some where T == Int { var minus10: Int { return base - 10 } func plus100() -> Int { return base + 100 } }この
minus10
とplus100
はT
がInt
の時のみ使えると言う条件を付け足しました。
base
はInt
なので、実際は1000
などが入ってきます。おわりに
メソッドチェーンはこのような考慮がかけています。ぜひ、この記事を参考に書き換えてみてください。
- 投稿日:2021-03-29T13:31:12+09:00
いい感じのランダムの色が欲しいなぁって時にこれ。
例えば10個のいい感じのランダムの色が欲しいって時。
ガチのランダムにすると意味わからない変な色が出来上がったりしてしまうので。
っていうものをメモ程度に残しておきます。
意外と使える!hueの部分の分母を欲しい個数にするだけです。
saturationとbrightnessに関しては自分の好みの色味にしてください。下記コードそのまま使用した際にはこんな感じの色が出力されます。
似てる色があるとか言わないで
#CC8B29
#ABCC29
#49CC29
#29CC6A
#29CCCC
#296ACC
#4929CC
#AB29CC
#CC298B
#CC2929
swift.swiftlet colorList = [ UIColor(hue: 0/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 1/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 2/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 3/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 4/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 5/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 6/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 7/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 8/10, saturation: 0.8, brightness: 0.8, alpha: 1), UIColor(hue: 9/10, saturation: 0.8, brightness: 0.8, alpha: 1) ]java.java/* * いい感じの色のリストを指定数分作成する * colorCount: 色の数 * brightness: 色味 0.0~1.0 * */ private int[] createRandomColorList(int colorCount, float brightness) { float [] f = new float[3]; f[1] = brightness; f[2] = brightness; int[] cList = new int[colorCount]; for (int i = 0; i < colorCount; i++) { f[0] = 360f * (i+1)/colorCount; cList[i] = Color.HSVToColor(f); } return cList; }
- 投稿日:2021-03-29T10:13:12+09:00
[Swift] 大文字のSelfまとめ
はじめに
小文字のselfはよく使いますが、大文字のSelfというのもあります。
大文字のSelfについてまとめてみました。型の内部で型自身へのアクセス
以下のように、
Self.value
で構造体A
に紐付いたvalueにアクセスすることができます。struct A { static let value = 10 func printValue() { print(Self.value) } }ちなみに、Selfを書かずに直接スタティックプロパティ(value)にアクセスしようとすると
Static member 'value' cannot be used on instance of type 'A'
このようなエラーが出てきてしまいます。スタティックメソッドでも同様です。
struct B { var name: String var age: Int static func isOld(_ x: Int) -> Bool { return x > 60 } func ageFunc() { print(Self.isOld(age)) } }ちなみに、
Self
を構造体名のB
と書き換えても同じことです。ここではSelf == B
ですね。プロトコルの定義での利用
プロトコルはそれに準拠させた型の振る舞いを事前に宣言しておくことができますが、プロトコル自身はどの型で準拠されるのかは知りません。例えるなら、車の大きさ、色、ハンドルの位置などは製造の段階で決める事ができますが、製造の段階では一体この車はどんな人が乗るのかは知りません。利用者(型)が購入(準拠)して初めてどんな人が乗るのか(プロトコル自身がどの型で準拠されるのか)を知る事ができます。
しかし、その型と同じ型のデータを返すメソッドや、同じ型同士を比較したい時もあります。その時にSelfキーワードを使います。注意点として、前項ではその定義を含む型自身をSelfと置いていましたが、本項では少し扱い方が異なります。つまり、プロトコル定義内でSelfを用いてもプロトコルがSelfというわけではありません。ここでのSelfとは、
プロトコルを採用した具体的な型 == Self
です。protocol C { associatedtype Element var x: Element { get } var y: Element { get } func replace() -> Self static func +(lhs: Self, rhs: Self) -> Self }このように、xとyを入れ替えるreplaceメソッド、左辺の右辺を加算するスタティックメソッドをprotocol Cで宣言しています。replaceメソッドの戻り値にSelfを指定しています。この時点ではどのような型に準拠するのかわからないためです。スタティックメソッド+も同様です。
構造体Dに準拠させてみましょう。struct D: C { typealias Element = Int var x: Int var y: Int func replace() -> D { return D(x: y, y: x) } static func +(lhs: D, rhs: D) -> D { return D(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } }このように、Selfのところが具体的なかたDに変わったのがわかります。このプロトコルCは他の型にも紐付く事ができるようにするため、できるだけ具体的な型はプロトコル宣言時に書きたくないためです。(associatedtypeもそのため)
クラス内での利用
プロトコルとは違い、メソッドの返り値の型としてのみ扱う事ができます。
class E { func someFunc() -> Self { return self } }エクステンションでの利用
前述の通り、プロトコルを準拠する型をSelfで参照できますが、一定の条件を満たす型のみプロトコルエクステンションを有効にしたいこともあります。
そのような条件を記述するときにSelfでその型を表す事ができます。protocol SomeProtocol { } class SomeClass { } extension SomeProtocol where Self: SomeClass { func someFunc() { print(type(of: self)) } } class SomeSubClass: SomeClass, SomeProtocol { } let someSubClass = SomeSubClass() someSubClass.someFunc()この例では、
SomeProtocol
を準拠する型が条件Self: SomeClass
つまり、SomeClassを継承している場合のみsomeFunc()
を利用する事ができます。
以下のように、SomeProtocol
に準拠していてもSomeSubClass2
はSomeClass
を継承していない(条件を満たしていない)ため、someFunc()
にアクセスしようとするとエラーが出てきてしまいます。class SomeSubClass2: SomeProtocol { } let someSubClass2 = SomeSubClass2() someSubClass2.someFunc()エラー
Referencing instance method 'someFunc()' on 'SomeProtocol' requires that 'SomeSubClass2' inherit from 'SomeClass'
おわりに
selfと違ってSelfはややこしいですね。
- 投稿日:2021-03-29T10:13:12+09:00
【Swift】大文字のSelfまとめ
はじめに
小文字のselfはよく使いますが、大文字のSelfというのもあります。
大文字のSelfについてまとめてみました。型の内部で型自身へのアクセス
以下のように、
Self.value
で構造体A
に紐付いたvalueにアクセスすることができます。struct A { static let value = 10 func printValue() { print(Self.value) } }ちなみに、Selfを書かずに直接スタティックプロパティ(value)にアクセスしようとすると
Static member 'value' cannot be used on instance of type 'A'
このようなエラーが出てきてしまいます。スタティックメソッドでも同様です。
struct B { var name: String var age: Int static func isOld(_ x: Int) -> Bool { return x > 60 } func ageFunc() { print(Self.isOld(age)) } }ちなみに、
Self
を構造体名のB
と書き換えても同じことです。ここではSelf == B
ですね。プロトコルの定義での利用
プロトコルはそれに準拠させた型の振る舞いを事前に宣言しておくことができますが、プロトコル自身はどの型で準拠されるのかは知りません。例えるなら、車の大きさ、色、ハンドルの位置などは製造の段階で決める事ができますが、製造の段階では一体この車はどんな人が乗るのかは知りません。利用者(型)が購入(準拠)して初めてどんな人が乗るのか(プロトコル自身がどの型で準拠されるのか)を知る事ができます。
しかし、その型と同じ型のデータを返すメソッドや、同じ型同士を比較したい時もあります。その時にSelfキーワードを使います。注意点として、前項ではその定義を含む型自身をSelfと置いていましたが、本項では少し扱い方が異なります。つまり、プロトコル定義内でSelfを用いてもプロトコルがSelfというわけではありません。ここでのSelfとは、
プロトコルを採用した具体的な型 == Self
です。protocol C { associatedtype Element var x: Element { get } var y: Element { get } func replace() -> Self static func +(lhs: Self, rhs: Self) -> Self }このように、xとyを入れ替えるreplaceメソッド、左辺の右辺を加算するスタティックメソッドをprotocol Cで宣言しています。replaceメソッドの戻り値にSelfを指定しています。この時点ではどのような型に準拠するのかわからないためです。スタティックメソッド+も同様です。
構造体Dに準拠させてみましょう。struct D: C { typealias Element = Int var x: Int var y: Int func replace() -> D { return D(x: y, y: x) } static func +(lhs: D, rhs: D) -> D { return D(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } }このように、Selfのところが具体的なかたDに変わったのがわかります。このプロトコルCは他の型にも紐付く事ができるようにするため、できるだけ具体的な型はプロトコル宣言時に書きたくないためです。(associatedtypeもそのため)
クラス内での利用
プロトコルとは違い、メソッドの返り値の型としてのみ扱う事ができます。
class E { func someFunc() -> Self { return self } }エクステンションでの利用
前述の通り、プロトコルを準拠する型をSelfで参照できますが、一定の条件を満たす型のみプロトコルエクステンションを有効にしたいこともあります。
そのような条件を記述するときにSelfでその型を表す事ができます。protocol SomeProtocol { } class SomeClass { } extension SomeProtocol where Self: SomeClass { func someFunc() { print(type(of: self)) } } class SomeSubClass: SomeClass, SomeProtocol { } let someSubClass = SomeSubClass() someSubClass.someFunc()この例では、
SomeProtocol
を準拠する型が条件Self: SomeClass
つまり、SomeClassを継承している場合のみsomeFunc()
を利用する事ができます。
以下のように、SomeProtocol
に準拠していてもSomeSubClass2
はSomeClass
を継承していない(条件を満たしていない)ため、someFunc()
にアクセスしようとするとエラーが出てきてしまいます。class SomeSubClass2: SomeProtocol { } let someSubClass2 = SomeSubClass2() someSubClass2.someFunc()エラー
Referencing instance method 'someFunc()' on 'SomeProtocol' requires that 'SomeSubClass2' inherit from 'SomeClass'
おわりに
selfと違ってSelfはややこしいですね。
- 投稿日:2021-03-29T09:47:16+09:00
SwiftUIチュートリアル3を丁寧に説明しながらやってみるよ。
おはよう。
だよ。
さてさて引き続き、SwiftUIチュートリアル3を勉強していくよ。
20分でできるって書いてあるけれど、調べならやっていると一日余裕で超えるね。能力差?
以前も書きましたが、チュートリアルに沿って構文とか意味とか覚えていこう!という趣旨なので、間違っているところとかあれば教えていただけると幸いです。教材
チュートリアル3:ユーザーからの入力を操ろう!
Section1: ユーザーのお気に入り登録をしちゃおう
「Landmark.swift」で作業をするよ。
項目にisFavoriteというお気に入りかどうかを判断する変数を定義するよ。point1-1
画像のように読み込んでいるlandmarkData.jsonにも同名の項目をつけることで、この項目名でjsonの項目とモデルのキーを結びつけているよ。続いて、「LandmarkRow.swift」で作業をするよ。
LandmarkRow.swiftif landmark.isFavorite { // point1-2 Image(systemName: "star.fill") .foregroundColor(.yellow) } ///以下略point1-2:systemName
「SF Symbols」というSwiftUIの中で使用できるアイコンのセットがあり、それを使用するときにsystemName:XX--アイコン名--XXという指定の仕方をする。
ちなみに「〜.fill」が塗りつぶしアイコンになるよ。Section2: リストにフィルターかけちゃおう
「LandmarkList.swift」で作業をするよ。
LandmarkList.swiftimport SwiftUI struct LandmarkList: View { //point2-1 @State private var showFavoritesOnly = true //point2-2 var filteredLandmarks: [Landmark] { landmarks.filter { landmark in (!showFavoritesOnly || landmark.isFavorite) } } var body: some View { NavigationView { List(filteredLandmarks) { landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } .navigationTitle("Landmarks") } } }point2-1:@State
読み書き可能な値として管理される場合に使うプロパティだよ、
ユーザーからの入力によってリストの見え方を変えたい場合、
@Stateをつけることで、変更を監視して、変更されるとその内容によってViewを再描画してくれるよ。
またこれはPrivateにして、呼び出しているViewそのものと、その子View内に限定して使用する必要があるよ。
参考:statepoint2-2:filter
配列.filter {要素 in 条件 }のような形で処理が書かれていて、
配列の1行1行が、条件に一致している場合はそれを返すようになっているよ。24行目のListに渡す値によって、見え方がこんな感じで変わります。
変更前:リストに対してlandmarksのデータ自体を渡しているため、全件表示されている。
変更後:リストに対して、filterをかけた結果を渡している。今回はshoFavoirteOnly = trueなので、絞り込まれた状態になる!
絞り込まれたね
Section3:Toggleボタンで表示を画面から操作してみよう!
次は、showFavoriteOnlyのtrue/falseの切り替えを画面から実施できるようにするよ。
landmarkList.swiftで作業をするよ。landmarkList.swiftvar body: some View { NavigationView { List { // point3-1 Toggle (isOn:$showFavoritesOnly) { //point3-2 Text("Favorites Only") } ForEach(filteredLandmarks) { landmark in //point3-3 NavigationLink( destination: LandmarkDetail(landmark: landmark)){ LandmarkRow(landmark: landmark) } } } .navigationTitle("Landmarks") } } }point3-1:List
動的な要素と静的な要素を組み合わせるために、Listに直接landmarksを渡す形から、Listの中でネストする形に変更する。point3-2:Toggle
Toggle(isOn : バインドする変数) { Toggleのラベル }のような形で使用します。
今回は$showFavoritesOnlyにバインドされている。
これでToggleがOn,OFFによってshowFavoritesOnlyのTrue,Falseが切り替わるようになったよ。point3-3:ForEach
showFavoritesOnlyが切り替わったら、リストもきりかえる必要があるため、ForEachを使う・・?のかな・・?
いまいちわからなかったので、もう少し調べます。Section4:ストレージを監視可能なオブジェクトにしよう。
Section4〜6ではユーザーがお気に入りをチェックしたり、外したりできるようにしていきます。
Section4ではまず、データモデルの整備をおこないます。
「ModelData.swift」で作業をするよ。ModelData.swift//point4-1 final class ModelData : ObservableObject { //point 4-2 @Published var landmarks: [Landmark] = load("landmarkData.json") } ///以下略point4-1 ObservableObject
値の変更などのイベントの発行と監視ができるのがCombineフレームワーク。
これを使用することで、イベント処理を組み合わせて、非同期処理を楽に処理できるんだなーと理解しました。
ObservableObjectはそのフレームワークに準拠したかたちのクラスであることを定義していると思われます。@Statusでも同じように値の変更をViewに知らせる役割があったけれど、それをオブジェクト単位で付与できるのがObservableObjectかな。
[参考:Combine]https://developer.apple.com/documentation/combine
参考:ObservableObjectpoint4-2 Published
Combineフレームワークの中で、ポイントとなってくるのが、「Publisher」と「Subscriber」の二つです。
Publisher = イベント、データを発行する側
Subscriber = イベント、データを受け取る側今回は、landmarkData.jsonの値の変更を監視して、何か変更された時Subscriberに知らせる必要があります。
なので、landmarksに@Publisherをつけて、Publisherとして定義してあげます。
参考:publisherSection5:ビューにモデルを使おう。
続いてはView側の修正をしていくよ。
チュートリアルとは順序が反対になるのですが、個人的にデータの流れを知りたいタイプなので、
Step6から実施します。
「LandmarksApp.swift」で作業します。LandmarksApp.swiftimport SwiftUI @main struct LandmarksApp: App { // point 5-1 @StateObject private var modelData = ModelData() var body: some Scene { WindowGroup { ContentView() .environmentObject(modelData) } } }point5-1:@StateObject
StateObjectは監視対象のオブジェクトをインスタント化する時に使用するよ。これを宣言することで、初期値の設定も行なっています。
そして作業しているLandmarksApp.swiftはアプリの数あるViewの中で最上位のViewだよね。
ここで、ModelData.swiftのModelDataクラスから「modelData」という新しいインスタンスを作成して、
ContentViewの環境オブジェクトとして渡しているよ。ContentView.swiftimport SwiftUI struct ContentView: View { var body: some View { LandmarkList() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() // point5-2 .environmentObject(ModelData()) } }point5-2:プレビュー用にenvironmentObject
次のContentViewでは実際のViewは特に変更はしなけれど、Previewに対しては同じように、
環境オブジェクトとして、ModelDateクラスを渡しているよ。
本来のアプリを起動した際に使用するルートでは、上位ViewですでにModelDataが渡されているね。
でも、開発中はView単体でプレビューすることもあり、そんな時だと「ModelDataがない! プレビューできない!」って失敗しちゃうので、
Viewごとに値を渡しているイメージかな。
同じように、LandmarkList.swift、LandmarkRow.swiftでも、プレビュー用に個々で値を設定しているよ。最後に、「LandmarkList.swift」での処理をみてみるよ。
LandmarkList.swiftstruct LandmarkList: View { // point 5-3 @EnvironmentObject var modelData : ModelData @State private var showFavoritesOnly = true var filteredLandmarks :[Landmark]{ // point5-4 modelData.landmarks.filter { landmark in (!showFavoritesOnly || landmark.isFavorite) } }変更を加えたのは、pointをつけた2箇所です。
point5-3:@EnvironmentObject
ObservableObjectに準拠したModelとViewをつなぐ役割で、監視対象のModelが変更されると、現在のViewを作成しなおす。
@ObservableObjectというものもあり、同じくObservableObjectプロトコルで作られたClassをView側で使う時につけるものです。
違いとしては、@EnvironmentObjectだと、View同士の階層を気にせずそのアプリ全体で使用することができることがあげられます。
なので、今回みたいにViewAがViewBを読み出して・・のように親子、祖父親子と階層が出来上がっていて、
かつ、データ自体は1箇所という場合には、EnvironmentObjectとして、設定した方が良いよね..ってことかな?point5-4
Model側でもModelDataクラスの中で、landmarkを定義したので、View側でも同じように読み出します。Section6:お気に入りボタンをつけよう!
さてさてここまでModelからViewにお気に入りのOn,OFFを知らせるための準備をしてきたので、
最後に実際にお気に入り処理を追加してみるよ。新しくSwiftUIファイル「FavoriteButton.swift」を作成するよ。
FavoriteButton.swiftimport SwiftUI struct FavoriteButton: View { // point6-1 @Binding var isSet : Bool var body: some View { // point6-2 Button(action: { isSet.toggle() }){ Image(systemName: isSet ? "star.fill":"star") .foregroundColor(isSet ? Color.yellow : Color.gray) } } } struct FavoriteButton_Previews: PreviewProvider { static var previews: some View { FavoriteButton(isSet: .constant(true)) } }これを「LandmarkDetail.swift」から呼び出すよ。
LandmarkDetail.swiftimport SwiftUI struct LandmarkDetail: View { @EnvironmentObject var modelData :ModelData var landmark: Landmark var landmarkIndex: Int { // point 6-3 modelData.landmarks.firstIndex(where: { $0.id == landmark.id })! } var body: some View { ScrollView { MapView(coordinate: landmark.locationCoordinate) .ignoresSafeArea(edges: .top) .frame(height: 300) CircleImage(image : landmark.image) .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { HStack { Text(landmark.name) .font(.title) .foregroundColor(.primary) //Point 6-1 FavoriteButton(isSet:$modelData.landmarks[landmarkIndex].isFavorite) } HStack { Text(landmark.park) Spacer() Text(landmark.state) } .font(.subheadline) .foregroundColor(.secondary) Divider() Text("About ¥(landmark.name)") .font(.title2) Text(landmark.description) } .padding() } .navigationTitle(landmark.name) .navigationBarTitleDisplayMode(.inline) } } struct LandmarkDetail_Previews: PreviewProvider { static let modelData = ModelData() static var previews: some View { LandmarkDetail(landmark : modelData.landmarks[0]).environmentObject(modelData) } }point6-1:Binding
バインディングを使用することで表示側のViewとデータを格納しているプロパティ、双方からの読み書きができるようになるよ。
まずは、FavoriteButtonの方で、スターのOn,Offによって、True,Falseを変更できるような処理を書いているよね。
そのボタンを、呼び出しているLandmarkDetail.swift側のPoint6-1がついている部分で
isSetとlandmark.isFavoriteを結びつけているため、
isFavoriteが初期値としてisSetに渡され、isSetが変わる=>isFavoriteが変わるようになるよ。
参考:Bindingpoint6-2:Button
Buttion(action : トリガーとなる処理){ ラベル }のように使用するよ。ラベル部分はText(XXX)ではなく、アイコンでisSetのTrue,Falseによってスターの塗りつぶしと色を変えているよ。
[参考:Button]https://developer.apple.com/documentation/swiftui/buttonpoint6-3:firstIndex(where:)
whereに条件を書いて、配列の中で一番最初に条件に一致するIndexを返すよ。
今回は配列のidと現在のLandmarkListから渡ってきたlandmark.idを比較して、一致しているIndexを返しています。
https://developer.apple.com/documentation/swift/array/2994722-firstindex以上で、チュートリアル3が終了です。
まとめ
StateやEnvironmentObjectなど、双方向のデータやり取りに必要なプロパティについて学んだよ。
なんとなーくのイメージしか掴めていないので、もっと調べて、使い分けとか自信持ってできるようになりたい・・・
また、今回はJSONから呼び出したけれど、APIなど外部から取得した値を使ってリストを表示する場合はどうやって書くのかな、とか
色々試してみたい箇所が出てきたよ。間違っている箇所や、こう理解すると良いよ、といったアドバイスがあれば教えていただけると幸いです。
では
- 投稿日:2021-03-29T09:11:39+09:00
10年前の世界からやってきたオッサンがCGIをSwiftで書いてみる
タイトルの意味(≒この記事の概要)
- およそ10年ぶりに個人サイト1を更新してみた。
- 全てのページをPerlによるCGIで生成していたが、Swiftで一から書き直した。
- しかし、サーバやCSS, HTMLに関する知識は10年前から変わっていない2。
- 即ち10年前からタイムスリップしてきたことと同じだった(誇張)。
それを踏まえてSwiftでCGIプログラムを書いた経緯をインタビュー形式で記事にします。なぜインタビュー形式なのか、それは永遠の謎です。
一応、CGIプログラムについて基礎的なことから書いてあります。Swiftに限らずいろんな言語でCGIを書くきっかけになれば…。
SwiftでCGI
そもそもCGIって…
通りすがりの知ったかぶりインタビュアー(以下I): CGIという言葉自体が死語になりつつあるようですが?
10年前の世界からやってきたオッサンことYOCKOW(以下Y): そうなんですか?昔は自分のサイトに掲示板(死語)やチャット(死語)を設置するためにフリーのCGIプログラムをもらってくることが流行った気がします。でも、自分が借りてるレンタルサーバーの環境に合わなくてプログラムを修正しなきゃいけなかったり、細かい仕様が気に入らなかったり…。それなら自分で最初から作っちゃえば良いじゃん、ってなってCGIのプログラムを書き書きし始める…というのが一般的な流れでした(N=1)。そういう意味でCGIという言葉はサイト運営をする上で切っても切り離せないものでした。
I: もちろん今もCGIという仕組みが無くなった訳ではないですが、動的なページを作るのにCGIの仕様を意識しなければいけない環境は減っていると言えます。Webアプリケーションのバックエンドを作るにしても便利なフレームワークがありますし、クラウド・アプリケーション・プラットフォームなどというものまでありますからね。
Y: ただ、個人サイトを作るだけなら、そんな大層なソフトウェアを使わなくても…という気はします(10年前の感覚)。だって、CGIは標準出力に文字列(データ)を書き出すことができればそれで良い訳ですから。もちろん
POST
を処理するなら標準入力も扱えないといけないですけど。なんならbashでだってCGIは書けます3。I: ところで、10年前にはSwiftは存在していませんでした4。当時は何の言語でCGIを?
Y: 当然Perlですね。CGIといえばPerl、PerlといえばCGIという時代でした。次点でPHPでしょうか。Yahoo!知恵袋に「CGIとPerlって何が違うんですか?」みたいな質問がいくつもありました5。
I: 「法律と警察って何が違うんですか?」みたいな質問ですね。
Y: 確かに質問として本来成り立たないはずのものですが、それだけCGIといえばPerlという“常識”があったのでしょう。
CGIプログラムの実際
I: “CGIは標準出力に文字列を書き出すことができれば良い”という話がありましたが、具体的にはどうすればいいのでしょうか?
Y: 厳密な話はRFC3875を読んでいただくとして、最低限
Content-Type
ヘッダ行と0バイト以上のボディーさえあればOKです。Perlで書くとsimple.cgi#!/usr/bin/perl print "Content-Type: text/plain; charset=utf-8\n"; print "\n"; print "Hello, CGI.";Y: これだけで"Hello, CGI."という文字を表示するCGIの完成です。ちなみに、HTTPヘッダの改行は必ず
CR+LF
でなくてはなりませんが、CGIプログラムの段階での改行コードはLF
で問題ありません。なぜなら、Webサーバが必ず改行文字をCR+LF
に変換してくれるからです6。I: ところで、上記のコードではHTTPレスポンスコードが指定されていませんが、どうなっているのでしょうか?
Y: 基本的に、CGIが正常終了すれば
200
、異常終了すれば500
ですね。I: え?それだけですか?
Y: まぁ、もちろん、それだけではないです。CGIでは
Status
という特別なフィールドが規定されています7。他のHTTPヘッダと同じようにStatus: 200 OK
などと出力すれば、Webサーバが良きに計らってくれて適切なHTTPレスポンスに変換されます。もちろん404ならStatus: 404 Not Found
とします。Reason Phraseを自分で決めることもできます。Status: 500 I have given up
とかでも問題ありません8。I: 文字列を出力するだけでいいなら、確かにどんな言語でも良さそうですね。
Y: そうですね。もちろんSwiftでもCGIプログラムを書くことが可能です。
cgi.swift#!/usr/bin/swift print(""" Status: 200 Swift is also OK Content-Type: text/plain; charset=utf-8 Hello, Swift CGI. """)I: Swiftでもshebangが使えるんですね。
Y: そうなんです9。ただ、shebangを使うと単一ファイルのコードしか実行できないですし、Swiftはお世辞にもコンパイルが早いとは言えないので(小声)、予めコンパイルした実行ファイルをCGIプログラムとして起動することをお勧めします。
CGIに使える言語
I: 今回CGIを一から作り直すにあたって、最初から言語をSwiftにすると決めていたのでしょうか?
Y: それを話すと少し長くなりますが良いですか?
I: (え、めんどくさい…)
Y: 私が個人サイト1を立ち上げたのは1999年の春でした。確か「フリーティケットシアター」(freett.com)10を利用していましたね。
I: (話し始めちゃった…)
Y: そして、CGIが使えるレンタルサーバに移ったのが2002年頃です。その頃から一部のページをCGIで表示するようになり、遅くとも2003年にはほぼ全てのページをCGIで出力するようにしました。たぶん。
I: 先の話にあったように、その時使っていた言語はPerlだった訳ですね。
Y: そうです。最初の頃はとほほのWWW入門にお世話になりました。
I: 今や老舗のサイトですね。
Y: 当時、Mac OS X(macOSの当時の名前)用のソフトウェアを公開することも自分のサイトの目的の一つでした。そして、Objective-Cを触っているうちに気づいてしまったのです。
I: と言いますと?
Y: オブジェクト指向は素晴らしい!
I: あぁー…。
Y: ただ、今思い返すと、オブジェクト指向そのものが素晴らしいというよりは、Cocoa (= Foundation + AppKit)が素晴らしいということだったとは思いますけどね。ちなみに、木下誠氏11の書籍やWeb連載が好きでした。
I: なるほど。
Y: 本当にFoundationが好きすぎて、PerlでFoundationを再実装するような行為に勤しんでいました。もちろん、完コピではなく自分が必要とする部分だけの実装でしたが。たとえば、
NSString
に倣って書いたHTB::String
(HTBは当時のサイト名にちなんだprefix)では、String.pmsub stringByReplacingOccurrencesOfString_withString_options_range { my $self = shift; my $target = shift; my $replacement = shift; my $options = shift; my $searchRange = shift; # 以下実際のコード # : }Y: というようにメソッド(Perl用語ではサブルーチン)の名前を付けていました。
I: Foundationにおけるネーミングそのままですね。
Y: ただ、Perl(Perl 5)は「オブジェクト指向のような書き方ができる」程度で、言語レベルでクラス定義などをサポートしていませんでした。…ですよね?当時、"Perl 6"が出る出ると言われて全然出なかったこともあり12、Perl以外の言語も考えていました。しかし、2008年ごろから現実世界が忙しくなってプログラミングに時間を割けなくなりました。さらに、2010年頃はゆるキャラや某音ゲーのことしか考えていませんでした。こうしてプログラミングのブランクが始まったのです。2012年に一念発起してCGIを一からPerlで書き直したのですが8割方完成したところで諸事情により再び中断することになりました。
I: (途中のよくわからない話は無視して)次にプログラミングを始めるのは?
Y: まずは2016年にVPS(Virtual Private Server)を借りることにしました。多少(時間や金銭に)余裕ができたので。レンタルサーバが公園の砂場なら、VPSは無人島13というところでしょうか。『何もないから、なんでもできる』みたいな。でも、すぐに何かをしたいというよりは、いつか何かできたらいいなという感じでした。最初、CGIに使う言語としてはRubyを考えていました14。日本発の言語で日本語の資料も豊富にあったというのも理由です。Rubyでのプログラミングは実際楽しかったです。Perlの
AUTOLOAD
15をRubyでも実現させてみたり。でも、『Rubyは遅い』みたいな話もあったりして。ここらへんは結局宗教論争のようなところはあるので、どうでもいいと言えばどうでもよかったのですが…。そもそも速さを求めるようなサイトではないですし。それでもなんとなく速さのことが気になって…。そこで、速さのことを考えるならそもそもインタプリタ方式ではなくてコンパイラ方式にすればええやんと思ったのです。VPSなら色んなコンパイラを導入できますしね。I: なるほど、それでSwiftに?
Y: いいえ、C言語 です。
I: まさかのC。…なぜSwiftではなかったのでしょう?Swiftは2015年末にはオープンソース化もされ、名目上Linux対応も謳っていましたが。
Y: ただ単にSwiftを知らなかったのです。Mac OS X用のソフトウェアを作ったのは2005年が最後でした16。そこからはMacソフト開発からは遠ざかっていたので、Swiftなどというものの存在に触れる機会がなかった訳です。結果的に、まずはC言語を試すことになりました。
I: そういえば、かの「2ちゃんねる」(現・5ちゃんねる)も軽量化のためにかつてPerlからC言語に移行したなんていうこともありました[要出典]。
Y: C言語でまずやり始めたのは…またFoundationの猿真似でした。2005年頃にPerlでやっていたことを再度始めたのでした。これも今思い返してみれば、Core Foundationを使えばそれで良かったんですよね。Objective-CのFoundationはプロプライエタリですが、C言語で書かれたCore Foundationはオープンソースかつクロスプラットフォームですからね。どうしても車輪の再発明が大好きな質なので、一から作ることを先に考えちゃうんですよね。まぁ、それはそれで楽しかったです。メモリ管理とか、ディクショナリのためのハッシュ関数を自作したりとか。楽しかったけど、もちろん、面倒でした。なので、C言語から別の言語を使うことを考え始めて…。
I: そこでSwift?
Y: いいえ、Objective-Cです。
I: おっと。もしかしてサーバをMacに…?
Y: Linuxです。Objective-Cは決してMac専用の言語ではありません。その誕生は1983年(または1984年?)と古く、GCCも標準でObjective-Cをサポートしています。もちろんFoundationは前出の通りプロプライエタリなのでLinuxでは使えませんが、GNUstepがあります。GNUstepはNeXTのOPENSTEPと互換性を持たせたフリーソフトウェアです17。ご存知の通り、NeXTはのちにAppleに買収され、OPENSTEPのObjective-CライブラリはCocoaと名前を変え開発が進められることになります。GNUstepはAppleのCocoaに追随しており、Mac, Linux両方で開発を進められると思っていたのです…。
I: 思って「いた」?
Y: はい。実際初期のコードはMacでもLinuxでも動作したのですが、FoundationのAPIをいろいろ調べているうちに違和感を覚えるようになったのです。…違和感の正体はObjective-C 2.0でした。Objective-C 2.0はMac OS X v10.5 Leopardとともに登場したものです。つまり2007年登場です。当時(2016年頃)で既に登場から10年ほど経っているので、Objective-C 2.0は当たり前のように使われていました。しかし、前述の通り私が最後にMac OS X用のソフトウェアを作ったのは2005年のことです。Objective-C 2.0などというものを知る由もありませんでした。調べていくと、Linuxでもlibobjc2を使えばObjective-C 2.0が利用できると分かったのですが、自分の中でObjective-C 2.0は新しい言語を覚えるような気分でした。一方で、FoundationのAPIをApple Developerのサイトで調べているときに気づいたことがありました。
I: それは…?
Y: ページの右上に"Language"を選択するメニューがあったのです。Languageといっても日本語や英語という意味ではなく、プログラミングの言語を選択するメニューです。…Objective-CとSwiftが並んでいました。そこから「Swiftって何だろう?」と調べるようになりました。その時、Swiftのバージョンは2.2で、もうすぐ3が登場するだろうというタイミングでした。最初に見た時は、一見してスクリプト言語のような印象だったので、AppleScriptの進化版か何かかと思っていました。でも、Qiitaで@koherさんや@omochimetaruさんの記事を読んだりして、思ったのです。「SwiftはObjective-Cを置き換えようとしている」と。実際にAppleがどういう意図をもってSwiftを開発しているのかはわかりませんでしたが、そう思った瞬間、自分のCGIプログラムの言語はSwiftにすることに決まりました。Linuxでも使えるということはもちろん、今後、Darwinプラットフォーム(macOSやiOS)のソフトウェアを開発するときにも必ず役に立つと考えたからです18。
I: やっとSwiftに辿り着きましたね(長かった…)。
CGIとSwift
Y: まずはLinuxでSwiftコンパイラを動かすところから始めました。当時はCentOSを使っていたのですが、CentOS用のバイナリが配布されておらず、Swiftを2時間ぐらいかけてビルドした記憶があります。しかもFoundation(Swiftで書かれたFoundation)のビルドに失敗してやり直しになるという…。のちにUbuntuに乗り換えたのですが、今はCentOS以外にもAmazon Linux 2用のバイナリまで配布されるなど、便利になりました。
I: 今はLinuxどころかWindows用までありますからね19。
Y: 当時既にSwiftはオープンソースになっていたのですが、Swiftで検索してもDarwin以外のプラットフォームで動かす情報が少なかったように思います。そこで、少しずつ自分が得た知見を共有しようと、Qiitaの記事もたまに書くようになりました。初めて書いた記事は、『C言語とSwiftを一つのプロジェクトで混在させてみる(OS X, Linux)』(2016年5月)です。Swift Package Managerがまだ無かった時代の記事ですね。ちなみに、これまでに投稿したSwiftに関する記事は全てDarwinでもLinuxでも動作するコードを前提に記載しています。
I: CGIだけであればSwiftUIとかは関係ないですからね。
Y: そしてSwiftに多少慣れたタイミングでCGIプログラムを作るためのライブラリを作り始めました。その名もSwiftCGIResponder。
CGIResponder
という単純なstructを用意して、CGIとしての出力を任せるというものです。I: 一応、その頃にはKituraもVaporも登場済みだとは思いますが…。
Y: そうだったんですね。全然気にしてませんでした。Perlの時代と同じように、一からCGIプログラムを書くことしか考えていませんでした。ただ、Perlの時代とは大きく違うことがありました。それは、Foundationが最初から存在するということです。PerlのときもRubyのときもCのときも、まず自分でFoundationっぽい何かを作るところから始めていました。Foundation大好きっ子ですからね。それがSwiftではLinuxでもFoundationが使えるのです。
I: Foundationがあれば充分だった、と。
Y: その通りです。ですが、それはそれで単純ではありませんでした。Darwinでは、Swiftで使うFoundationはObjective-CのFoundationをインポートする形です。即ちCocoaを(原則)そのままSwiftでも使えます。しかし、Darwin以外のプラットフォームでは、Swiftで書かれたFoundation20を使うことになります。そのSwiftFoundationが、バグと未実装の嵐だったのです。
I: となると、Swift以外の言語へ移ることも検討を?
Y: そんなことはしません。Foundation大好きっ子なので(2回目)。バグがあるなら直せば良いじゃないですか。幸いにもSwiftはオープンソースです。GitHubでPRを送ることもできます。というわけで、あると困るバグは自分で直しました。こうしてFoundationを使ってCGIプログラムを作るという、夢のような世界に到達できたのです。今のところ、SwiftCGIResponderを使うと、
CGIResponserSample.cgiimport CGIResponder var responder = CGIResponder() responder.status = .ok responder.contentType = ContentType(pathExtension: .txt, parameters: ["charset": "UTF-8"])! responder.content = .string("Hello, World!\n", encoding: .utf8) try! responder.respond() // -- Output -- // Status: 200 OK // Content-Type: text/plain; charset=UTF-8 // // Hello, World! //Y: のように書くことができます。基本的に
CGIResponder
は単純で、上のコードを見るだけだと「直接文字列を出力すればいいのでは?」となりそうですが、その裏では、HTTP Headerを適切に扱うような仕組み・国際化ドメイン名を含めたドメインの処理・HTTP CookieとPublic Suffixの処理・XHTMLパーサ/ジェネレータなどを実装してあります。ただ、コードサイズが大きくなってしまったので、結果として、少しずつ機能を切り離して独立したパッケージにしました。見かけ上、依存するパッケージが多くなりましたが、全部自分で作ったものという…。I: 車輪の再発明も含まれていそうですね…。
Y: そうですね。でも、車輪の再発明も大好きなので。こうして、2008年ごろから更新が滞り始め2012年には完全に止まってしまったサイトの更新を2021年に再開することができました21。サイトの更新が目的となってしまっているので中身はないですし、CSSやHTMLの知識は昔のままですが。気付いたら、HTMLはフロントエンドで生成することが主流になっているようで(?)、完全に時代に取り残されていますね22。本当はMarkdownのパーサもSwiftで書きたかったのですが、一旦はフロントエンド側で処理することにしました。ただ、その時には「JavaScript怖い」という思いは強かったです。
I: 何が怖かったのでしょう?
Y: 動的型付けだからです。Swiftを長く触ったあとに久しぶりにJavaScriptを触ったので、コード上に型表記できないことやnull safetyがないことに強烈な恐怖心を覚えました。そこからTypeScriptを導入するまではすぐでした。Swiftの素晴らしさの再発見でもありますね。
おわりに: SwiftはCGIに相応しい言語か
I: Swift(というかFoundation)への想いは強いようですが、SwiftはCGIプログラムに相応しい言語と言えるでしょうか?
Y: 現在のところ最も相応しいですね、私にとっては。当たり前ですが、言語の選択は「誰が何のためにどのような環境で動かすプログラムなのか」によって変わってくるものですから。CGIプログラムに限らない話ですね。最初のほうの話にもあったように、CGIプログラムは標準出力と標準入力を扱うことができればそれで良いのです。bashが一番自分にとって書きやすいのであればbashで書けば良いと思いますし、Cが良ければCで書けば良いという話です。少なくともフレームワークがないと何もできないということはあり得ません。まずは
ここでいう「個人サイト」とは、「自分の自分による自分のためのサイト」のこと。 ↩
実際には2003年ぐらいの知識で止まっているような感覚。そして、もともと知識は乏しい。 ↩
他の例→Perlから始めないCGI入門 ↩
Swiftは2014年6月登場。 ↩
実例→ https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q147029824 ↩
HTTP/2だとreason phraseが空文字列に…? ↩
2016年3月末でサービス終了 ↩
現エイチエムディティ株式会社社長。 ↩
2000年に設計が始まったPerl 6の正式リリースは2015年12月25日のこと。結局Perl 5との互換性はなく、のちに言語名がRakuに変わった。 ↩
専サバに比べれば狭い無人島なのでしょうが…。 ↩
もちろんRailsは使わない ↩
参考記事『AUTOLOADについて』 ↩
最後に作ったソフトは"DKT"というディベート甲子園専用のタイマー。2005年に愛・地球博で開催された第10回大会で実際に使用した。 ↩
Unix系OSだけでなくWindowsでも動作する。 ↩
結局開発してませんが。 ↩
多くはCore FoundationをSwiftでラップしたもの。 ↩
いつまた止まるか分かりませんが。 ↩
実は2012年に作り直した(作り直そうとした)サイトはJavaScriptでページを生成する仕組みでした。しかし、当時のGoogleクローラは動的ページに対応しておらず(たぶん)、JavaScript非対応のクライアントにはPerlでページを生成して返していました。つまり、PerlとJavaScriptで同じ結果を生み出すための同じコードを書かなくてはならず、2倍の作業量になっていたのです。時代を先取りしすぎたのかもしれません…。 ↩
- 投稿日:2021-03-29T08:43:11+09:00
【Swift】遷移先のTabBarControllerを非表示にする
どういうことか
遷移先ページの下のタブバーを非表示にしたい。
NextViewController.swiftself.tabBarController?.tabBar.isHidden = true遷移先の
viewDidLoad()
にこれを書いても非表示にはなるが、なんらかの処理で遷移元に戻っても消えたままになってしまう。遷移する処理に追加する
ViewController.swiftlet storyboard = UIStoryboard(name: "NextViewController", bundle: nil) let vc = storyboard.instantiateViewController(identifier: "NextViewController") // コレ vc.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(vc, animated: true)おわり(´・ω・`)
- 投稿日:2021-03-29T07:16:39+09:00
try Swift:スケジュール画面の解析
はじめに
try Swiftのスケジュール画面を解析する。ゴールは登場するクラスの洗い出しと関連性のざっくりとした把握。
スケジュール画面のスクリーンショット
スケジュール画面の構成及び構成する主なクラス
画面構成
[SessionTableViewCell達]
↑
[SessionTableViewController1, SessionTableViewController2, SessionTableViewController3]
↑
ScheduleViewControllerUI部分
ScheduleViewController:スケジュール画面の母体
ButtonBarView:画面上部のバーボタン(ボタンによって画面のコンテンツが表示される)
SessionTableViewController:画面のコンテンツが表示される画面
SessionTableViewCell:スケジュールコンテンツ画面モデル部分
ConferenceDay:会議日付及びSession情報を含む構造体
SessionBlock:Sessionsのスタート、エンド、Session情報達
Session:Session情報