20200403のiOSに関する記事は8件です。

簡単にFirebase Storageに複数のファイルをアップロードする[swift]

概要

複数のファイルのアップロードを以下のように扱えるようにStorageUploaderを作りました。どれくらいアップロードされているのか監視もできます。

sample.swift
let uploader = StorageUploader()

// アップロードしたいファイルを追加
uploader.addFile(file:URL.init("video"), childRef:videoChildRef, metadata:videoMetadata)
uploader.addData(data:imageData, childRef:imageChildRef, metadata:imageMetadata)

// アップロード状況の監視
uploader.progressHandler = {completedCount, totalCount in
    print("\(100 * completedCount / totalCount)%")
}

// アップロード完了時の処理
uploader.completeHandler = { metadatas in
    print("complete")
}

// アップロード失敗時の処理
uploader.failureHandler = { error in
    // switch (StorageErrorCode(rawValue: error.code)!) { ...
    print("fail")
}

// アップロード開始
uploader.start()

StorageUploaderのコード

StorageUploader.swift
import Foundation
import FirebaseStorage

class StorageUploader {
    fileprivate struct SUComponent {
        var file:URL?
        var data:Data?
        var childRef:StorageReference?
        var metadata:StorageMetadata?
        var isComplete = false
    }

    var progressHandler: (_ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void = {_, _ in }
    var completeHandler: (_ metaDatas: [StorageMetadata]) -> Void = {_ in }
    var failureHandler: (_ error: NSError?) -> Void = {_ in }

    fileprivate var components = [SUComponent]()
    fileprivate var uploadTasks = [StorageUploadTask]()
    fileprivate var totalUnitCount:Int64 = 1
    fileprivate var completedUnitCount:Int64 = 0
    fileprivate var isfailure = false

    func addFile(file:URL, childRef:StorageReference, metadata:StorageMetadata) {
        var uploadInfo = SUComponent()
        uploadInfo.file = file
        uploadInfo.childRef = childRef
        uploadInfo.metadata = metadata
        components.append(uploadInfo)
    }

    func addData(data:Data, childRef:StorageReference, metadata:StorageMetadata) {
        var uploadInfo = SUComponent()
        uploadInfo.data = data
        uploadInfo.childRef = childRef
        uploadInfo.metadata = metadata
        components.append(uploadInfo)
    }

    func start() {
        prepareUpload()
        progressHandler(completedUnitCount, totalUnitCount)
    }

    fileprivate func prepareUpload() {
        components.forEach { uploadInfo in
            let childRef = uploadInfo.childRef
            let metadata = uploadInfo.metadata
            if let file = uploadInfo.file {
                guard let uploadTask = childRef?.putFile(from: file, metadata: metadata) else {
                    return
                }
                observeStatus(uploadTask)
            } else if let data = uploadInfo.data {
                guard let uploadTask = childRef?.putData(data, metadata: metadata) else {
                    return
                }
                observeStatus(uploadTask)
            }
        }
    }

    fileprivate func observeStatus(_ uploadTask: StorageUploadTask) {
        uploadTasks.append(uploadTask)
        uploadTask.observe(.progress) { snapshot in
            var currentCompletedUnitCount:Int64 = 0
            var currentTotalUnitCount:Int64 = 0
            self.uploadTasks.forEach { uploadTask in
                currentCompletedUnitCount += (uploadTask.snapshot.progress?.completedUnitCount ?? 0)
                currentTotalUnitCount += (uploadTask.snapshot.progress?.totalUnitCount ?? 0)
            }
            self.completedUnitCount = currentCompletedUnitCount
            self.totalUnitCount = currentTotalUnitCount
            self.progressHandler(currentCompletedUnitCount, currentTotalUnitCount)
        }
        uploadTask.observe(.success) { snapshot in
            if self.completedUnitCount == self.totalUnitCount {
                let metaDatas = self.uploadTasks.map { uploadTask -> StorageMetadata in
                    return uploadTask.snapshot.metadata ?? StorageMetadata()
                }
                self.completeHandler(metaDatas)
            }
        }
        uploadTask.observe(.failure) { snapshot in
            if !self.isfailure {
                self.isfailure = true
                self.uploadTasks.forEach { uploadTask in
                    if uploadTask.snapshot.progress?.isCancellable ?? false {
                        uploadTask.cancel()
                    }
                }
                let error = snapshot.error as NSError?
                self.failureHandler(error)
            }
        }
    }
}

エラーハンドリング

エラーから以下の情報がわかる。

sample.swift
switch (StorageErrorCode(rawValue: error.code)!) {
case .unknown:
    // An unknown error occurred
    break
case .objectNotFound:
    // No object exists at the desired reference
    break
case .bucketNotFound:
    // No bucket is configured for Firebase Storage
    break
case .projectNotFound:
    // No project is configured for Firebase Storage
    break
case .quotaExceeded :
    // Quota on your Firebase Storage bucket has been exceeded
    // If you're on the free tier, upgrade to a paid plan
    // If you're on a paid plan, reach out to Firebase support
    break
case .unauthenticated:
    // User is unauthenticated. Authenticate and try again
    break
case .unauthorized:
    // User is not authorized to perform the desired action
    // Check your rules to ensure they are correct
    break
case .retryLimitExceeded :
    // The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded
    // Try uploading again
    break
case .nonMatchingChecksum :
    // File on the client does not match the checksum of the file received by the server
    // Try uploading again
    break
case .downloadSizeExceeded :
    // Size of the downloaded file exceeds the amount of memory allocated for the download
    // Increase memory cap and try downloading again
    break
case .cancelled:
    // User cancelled the operation
    break
case .invalidArgument :
    // An invalid argument was provided
    break
    /* ... */
default:
    // A separate error occurred. This is a good place to retry the upload.
    break
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CoreDataを使ったTableViewでスワイプ削除を行うには

■はじめに

IntやStringの配列をTableViewに表示していてスワイプ削除を行いたい場合はこちらをお勧めします=>(スワイプでtableviewのセル削除)

CoreDataとTableViewはセットで使われることが多いです。

CoreDataを扱っている記事のほとんどがTableViewで取得したデータを表示しているためその辺でつまづいてもいくつか記事をあさって行けば問題ありません。

しかし、TableViewのCellをスワイプで削除する方法についての記事は少なく、どの記事も古い。しかもCoreDataとセットで扱ってる記事がないので、雑なコードだけでも載せておきますので、参考にしてみてください。

■動作環境

  • Xcode Version 11.3.1
  • Swift5

■ソースコード

ViewController.swift
//〜〜
//割愛
//〜〜

// セルの削除許可を設定
    func tableView(_ tableView: UITableView,canEditRowAt indexPath: IndexPath) -> Bool{
        return true
    }

// スワイプ削除
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

        do {
            let fetchRequest = NSFetchRequest<NSManagedObject>(entityName:"FoodInformation")

            // 条件指定                                  ↓条件(Int型は%d)↓                 ↓値↓
            fetchRequest.predicate = NSPredicate(format: "foodId = %d", CoreDataArray[indexPath.row].foodId)
            let data = try managedObjectContext.fetch(fetchRequest)

            // 指定した条件の内容を削除
            for task in data {
                managedObjectContext.delete(task)
            }

            // 削除したDBの変更内容を保存
            (UIApplication.shared.delegate as! AppDelegate).saveContext()

            // CoreDataArrayはこのクラスの変数。DBよりあとに削除
            self.CoreDataArray.remove(at: indexPath.row)  //(※1)

        } catch {
            print("Fetching Failed.")
        }

        // TableViewのCellを削除
        tableView.deleteRows(at: [indexPath], with: .automatic)
    }

//〜〜
//割愛
//〜〜

僕がつまづいたのは(※1)のCoreDataを保存した配列の削除の部分です。他の記事でこの部分を先に書くというものがありました。しかしそれはCoreDataを使うことを考慮していないものだったのでかなり惑わされてしまいました(笑)

このソースコードを、DelegateとかDataSourceとかセルのタップアクションとかみたいにはっつければ動きます。

entityNameや条件などは自身のソースコードに合うように変更してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift5】CoreDataを使ったTableViewでスワイプ削除を行うには

■はじめに

IntやStringの配列をTableViewに表示していてスワイプ削除を行いたい場合はこちらをお勧めします=>(スワイプでtableviewのセル削除)

CoreDataとTableViewはセットで使われることが多いです。

CoreDataを扱っている記事のほとんどがTableViewで取得したデータを表示しているためその辺でつまづいてもいくつか記事をあさって行けば問題ありません。

しかし、TableViewのCellをスワイプで削除する方法についての記事は少なく、どの記事も古い。しかもCoreDataとセットで扱ってる記事がないので、雑なコードだけでも載せておきますので、参考にしてみてください。

■動作環境

  • Xcode Version 11.3.1
  • Swift5

■ソースコード

ViewController.swift
//〜〜
//割愛
//〜〜

// セルの削除許可を設定
    func tableView(_ tableView: UITableView,canEditRowAt indexPath: IndexPath) -> Bool{
        return true
    }

// スワイプ削除
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

        do {
            let fetchRequest = NSFetchRequest<NSManagedObject>(entityName:"FoodInformation")

            // 条件指定                                  ↓条件(Int型は%d)↓                 ↓値↓
            fetchRequest.predicate = NSPredicate(format: "foodId = %d", CoreDataArray[indexPath.row].foodId)
            let data = try managedObjectContext.fetch(fetchRequest)

            // 指定した条件の内容を削除
            for task in data {
                managedObjectContext.delete(task)
            }

            // 削除したDBの変更内容を保存
            (UIApplication.shared.delegate as! AppDelegate).saveContext()

            // CoreDataArrayはこのクラスの変数。DBよりあとに削除
            self.CoreDataArray.remove(at: indexPath.row)  //(※1)

        } catch {
            print("Fetching Failed.")
        }

        // TableViewのCellを削除
        tableView.deleteRows(at: [indexPath], with: .automatic)
    }

//〜〜
//割愛
//〜〜

僕がつまづいたのは(※1)のCoreDataを保存した配列の削除の部分です。他の記事でこの部分を先に書くというものがありました。しかしそれはCoreDataを使うことを考慮していないものだったのでかなり惑わされてしまいました(笑)

このソースコードを、DelegateとかDataSourceとかセルのタップアクションとかみたいにはっつければ動きます。

entityNameや条件などは自身のソースコードに合うように変更してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今さらまとめるObjective-C文法基礎

案件でObjective-Cに携わることになり、今さらながらObjective-Cの構文を学習したので備忘録としてまとめました。
※ SwiftでiOSアプリを作ったことがある人向けです。

変数・定数・型

宣言構文

// (1) 型を宣言をして代入する 
int age
age = 23;

// (2) 型宣言と同時に代入も可能
int age = 23;

// (3) 同じデータ型の場合は複数同時に宣言可能
int width = 100, height = 88, area;

基本データ型

データ型 説明
short int 16ビット符号付き整数
int 32ビット符号付き整数
long int 64ビット符号付き整数
float 32ビット符号付き少数。代入時数値の末尾にfが必要
double 64ビット符号付き少数
BOOL 真偽値。YES または NO で表現する
char 半角英数字1文字のみ代入可能。 代入する文字はシングルクォーテーションで囲む

short int, int, long int型のビット数はビルド環境によって変化するらしい。
※ Bool型は true/false でも真偽値になる。また、0NOそれ以外の数値YES になる。

ポインタ型(参照型)

基本データ型以外は全てポインタ型になる。

// ポインタ型変数として宣言する変数名の前に * (アスタリスク)をつける
NSString *name = @"太郎";

id型

どんな型でも代入可能な汎用的な型(Javaで言う Object型 )。
参照型の1つだが、宣言時に * が不要。

定数

const

変数宣言時にconstをつけることで定数を定義可能。
通常、ヘッダファイルまたはメインファイルの @interfece 文で宣言する。

// const はデータ型の前後どちらにつけても良い
const int LEVEL = 27;
NSString * const COLOR = @"green";

#define(マクロ定義)

#defineで指定された文字はコンパイル前に指定した値で書き換えられる。
#undefを使うと、それ以降は文字の書き換えが行われなくなる。

// ; は不要
#define LIMIT 100
#define URL @"hoge.com"
// マクロ関数も定義可能
# define test(a, b) a + b
NSLog(@"%d", test(1, 2));    // => 3

enum

連続する整数を定義可能。

enum {
  NUM_A = 10,
  NUM_B,    // => 11
  NUM_C,    // => 12
  NUM_D,    // => 13
  NUM_E = 99, 
  NUM_F,    // => 100
}

typedefを使ってenumをデータ型として定義する。

typedef enum {
  SUNNY,    // => 0
  CLOUDY,   // => 1
  RAINY,    // => 2
} Weather;

NS_ENUM を使う(この書き方が主流)

typedef NS_ENUM(NSInteger, Weather) {
  WeatherSUNNY,    // => 0
  WeatherCLOUDY,   // => 1
  WeatherRAINY,    // => 2
};

NS_ENUM の利点

  • switch文で記述漏れがないかコンパイラがチェックしてくれる
  • 型を明示する必要があるため、より型安全

文字列 - NSString

// 宣言 + 代入
NSString *name = @"太郎";

// 比較
if ([@"hoge" isEqualToString:@"huga"]) {    //=> false
}

// 連結
NSString *userName = [name stringByAppendingString:@" 様"];

// 文字数カウント
NSInteger length = [name length];
// これでもOK プロパティはないがコンパイラが引数なしのメソッドと判断する
NSInteger length = name.length

// 数値 → 文字列
NSString *age = [NSString stringWithFormat:@"%d", num];

// 文字列 → 数値
NSInteger *num = [@"1000" integerValue];

※ 文字列に変更を加えたい場合は NSMutableString クラスを使う

配列 - NSArray, NSMutableArray

  • NSArray : 要素の個数が固定
  • NSMutableArray : 要素の個数が可変
// 宣言 + 代入
NSArray *alphabet = @[@"A", @"B", @"C"];
// 配列の要素はオブジェクトのみ 数値型やBOOL型を入れたい場合はNSNumberクラスとして入れる
int num = 10;
NSArray *numbers = @[@23, @190, @4, @(num)];

// 参照
NSString *b = alphabet[1];
// 末尾要素の参照(配列が空の場合はnil)
NSString *lastAlphabet = [alphabet lastObject];

// 要素数の取得(count はプロパティではなくメソッドだが、この書き方が可能)
int count = alphabet.count

// -- 以下、NSMutableArrayのみ

// 要素の追加
// 空の配列はNSMutableArrayクラスの親クラスのNSArrayクラスの array メソッドで作成可能
NSMutableArray *array [NSMutableArray array];
[array addObject:@"hoge"];

// 要素の挿入
NSMutableArray *greets [NSMutableArray arrayWithArray:@[@"こんにちは", @"Hello", @"Moi"]];
// atIndex番目に挿入する(先頭: 0)
[greets insertObject:@"你好" atIndex:1];    // => (こんにちは, 你好, Hello, Moi)

// 要素の削除
[greets removeObjectAtIndex:1];       // => (こんにちは, Hello, Moi)
[greets removeObject:@"こんにちは"];    // => (Hello, Moi)

辞書 - NSDictionary, NSMutableDictionary

// 宣言 @{キー:値, ...} の形式で書く キーはNSString, 値はオブジェクトのみ
NSDictionary *wether = @{@"sunny":@"晴れ", @"cloudy":@"曇り", @"rainy":@"雨"};
// 参照
NSLog(@"%@", wether[@"sunny"]);    // => 晴れ

// -- 以下、NSMutableDictionaryのみ

// 要素の追加
NSMutableDictionary *hoge = [NSMutableDictionary dictionaryWithCapacity:3];
hoge[@"A"] = @1;
hoge[@"B"] = @2;

// 値の変更(キーがない場合は追加になる)
hoge[@"B"] = @2222;

// 要素の削除(キーがない場合は何も起きない)
[hoge removeObjectForKey:@"A"];
[hoge removeAllObjects];

構造体 - Struct

// メンバにARCで管理されているオブジェクトを持つことはできない
// 定義(typedefを使って型として定義されることが多い)
struct _screen {
  unsigned int width;
  unsigned int height;
} Screen

// 宣言 + 代入
Screen iPhone;
iPhone.width = 5;
iPhone.height = 24;

制御構文

条件分岐 - if

条件式の結果が0, NO, NULL,nil`以外の時、if文の中を実行する。

// 普通にif文が書ける ※else ifもモチロン書ける
int age = 35;
if (age >= 20) {
  NSLog(@"You can drink.");
} else {
  NSLog(@"You cannot drink.");
}
// => You can drink.

条件分岐 - switch

case文の値は数値, char, 定数のみ設定可能。

int rank = 'B';
switch(score) {
  case 'A':
    NSLog(@"Your rank is A.");
    break;
  case 'B':
    NSLog(@"Your rank is B.");
    break;
  case 'C':
    NSLog(@"Your rank is C.");
    break;
  default:
    NSLog(@"You are not ranker...");
    break;
}
// => Your rank is B.

繰り返し - for

// カウンタを使う
for (int i = 0; i < 10; i++) {
  NSLog(@"%d", i);
}

// コレクションクラスのfor inを使う
NSArray *array = [@1, @2, @3];
NSInteger sum = 0;
for (NSNumber *num in array) {
  sum += [num intValue];
}
NSLog(@"%d", sum);

// NSArrayでenumerateObjectsUsingBlockを使う(推奨)
NSArray *array = [@1, @2, @3];
__block NSInteger sum = 0;
[array enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) {
  sum += obj;
}];
NSLog(@"%d", sum);

NSArrayの enumerateObjectsUsingBlock メソッドについては、こちらの記事の解説が分かりやすいです。
NSArrayでfor(; ;)とかfor-inを使うのをやめて、enumerateObjectsUsingBlock:を使う

繰り返し - while

条件式がYESの間繰り返す。

while (YES) {
  // 無限ループ
}

ログ出力 - NSLog

// NSLog(文字列オブジェクト);
NSString *name = @"太郎";
NSLog(str);    // => 太郎
// NSLog(フォーマット指定子を含んだ文字列オブジェクト, 値1, 値2, ...);
NSLog(@"こんにちは、%s", name);    // => こんにちは、太郎

// BOOL型の値は %d でフォーマットし、YES=1, NO=0を出力する
BOOL isOK = YES;
NSLog(@"Are you OK? %d", isOS);    // => Are you OK? 1

クラス構文

クラス定義はヘッダファイル hoge.h とメインファイル(実装ファイル) hoge.m に分かれて定義される。

ヘッダファイルの構造

hoge.h
#import <フレームワーク/ファイル>
#import ソースファイル

// @interface クラス名 : 継承元クラス名 <プロトコル>
interface Hoge : Huga <MyProtcol> {
  // インスタンス変数宣言
  // データ型 変数名;
  NSInteger num1, num2;
  NSArray *array;  
}
// プロパティ変数宣言
// @property (属性) データ型 プロパティ名;
@property (assign, readonly, noatomic) NSInteger * num;
// assign: 参照ではなく値を割り当てる
// strong: ARCでカウントする参照(初期値)
// weak  : ARCでカウントしない参照
// copy  : オブジェクトをコピーする
// --
// readonly : 読み出しのみ
// readwrite: 読み書き可能(初期値)
// --
// noatomic   : アトミック(別スレッドから同時に参照された際に同値を保証する状態)ではない(初期状態はアトミック)
// getter=ゲッタ名: 独自のゲッタメソッドを指定(noatomicを指定した場合のみ)
// setter=セッタ名: 独自のセッタメソッドを指定(noatomicを指定した場合のみ) 

// メソッド宣言
// + (戻り値の型) メソッド名:(引数1の型)引数1 ラベル2:(引数2の型)引数2 ...;
// - (戻り値の型) メソッド名:(引数1の型)引数1 ラベル2:(引数2の型)引数2 ...;
// + はクラスメソッド
// - はインスタンスメソッド
- (NSString *) fullName:(NSString *)firstName lastName:(NSString *)lastName;
@end

メインファイルの構造

hoge.m
#import ヘッダファイル

// @interface クラス名() <プロトコル>
// @interface ~ @end はクラスエクステンションと呼ぶ
// クラス内のみで使えるインスタンス変数、プリパティ変数、メソッドを宣言できる(子クラスからも使えない)
@interface Hoge() <MyProtcol> {
  // インスタンス変数宣言
}
// プロパティ変数宣言
// メソッド宣言
@end 

// @implementation クラス名
@implementation Hoge
// @implementation ~ @end はインプリメンテーションセクションと呼ぶ
// ヘッダファイル, クラスエクステンションで宣言したプロパティの初期化やメソッドの定義を行う
- (NSString *) fullName:(NSString *)firstName lastName:(NSString *)lastName {
  return [NSString stringWithFormat:@"%@ %@", firstName, lastName];
}
- (void) countup {
  // 自身のプロパティにアクセスする際は _ をつける か self をつける
  _count++;    // self.count++; でもOK
}
@end


クラスの使い方

// インスタンスの作成/初期化 alloc でメモリ確保 init で初期化
NSDate *theDate = [[NSDate alloc] init];

// コンビニエンスコンストラクタ
// 例えば、NSDateクラスでは、allocし現在時刻でinitしたNSDate型を返すdateクラスメソッドが用意されている
NSDate *today = [NSDate date];

// プロパティへのアクセス
int huga = myObj.hoge;

// メソッドの実行(メッセージ式) [クラス名/インスタンス名 メソッド名:引数];
[hogeObj hogeFunc:100];
// 複数引数の場合はラベルをつける 引数1のラベルがメソッド名なことに注意
[hogeObj hogeFunc:10 a:82 b:@"b"];

参考文献

  • 大重美幸, 詳細!Objective-C iPhoneアプリ開発入門ノート Xcode5+iOS7対応, 株式会社 ソーテック社, 2013.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのAmazonリンクからAmazonアプリを起動する方法

Info.plistにLSApplicationQueriesSchemesというキーでTypeをArrayにし、そこにアイテムを追加し、TypeをStringでvalueをcom.amazon.mobile.shopping.webにします

アソシエイトリンクの場合は、短縮URLhttps://amzn.to/xxxを使うとAmazon モバイルは現在amzn.toでご利用いただけませんというエラーがでるので通常URLを使う。

urlString = "アソシエイトリンクの通常URLからhttps://を除いた文字列"
let urlStringWithSchema = "com.amazon.mobile.shopping.web://" + urlString
            guard let urlWithSchema = URL(string: urlStringWithSchema) else { return }
            if UIApplication.shared.canOpenURL(urlWithSchema){
                UIApplication.shared.open(urlWithSchema, options: [:], completionHandler: nil)
            } else {
                guard let url = URL(string: urlString) else { return }
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }

一度アプリの該当画面からAmazonアプリを開く許可をすると、以後は許可を求められずに勝手にAmazonアプリが開く。これは Cunstom URL Schemas

Apple Developer のリファレンス等にあるような、URL Typesに値を追加する云々は、あくまでSafariなどでブラウジングしていて特定のURLを踏んだときに自社アプリを起動させるための設定なので混同しないように。

参考文献

https://lima.world/iphone-url/

https://qiita.com/zakiyamaaaaa/items/96c6fb8ee5a93c963bbc

https://stackoverflow.com/questions/27693062/iphone-ios-amazon-scheme-uri-not-working-anymore-amzn-version-4-3-1-deep-li
amzn://content/item?id=<some valid id>という方式は使えなくなったようだ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[swift]textViewにplaceHolder的なのをつけてみる

はじめに

こんにちは。swiftはじめて約1年のMomijiです!
今回ははじめての記事となるのですが、textViewにplaceHolder的なのをつけてみるということで書かせていただきます。
⚠️かなり自己流です。
textFieldにはあって、なんでtextViewにはplaceHolderないんでしょうね〜

コード

とりあえず、全体のコードです。

import UIKit

class SSubjectsDetailViewController: UIViewController,UITextViewDelegate {
    @IBOutlet weak var textView: UITextView!

    override func viewDidLoad() {
        textView.text = "内容を入力"
        textView.textColor = .lightGray
        textView.delegate = self
    }

    func textViewDidBeginEditing(_ textView: UITextView) {
        textView.text = ""
        textView.textColor = .black
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text == "" {
            textView.text = "内容を入力"
            textView.textColor = .lightGray
        }
    }
}

解説

textView.text = "内容を入力"
textView.textColor = .lightGray

内容を入力のところは、ご自分でpalceHolderに設定したい文字を入れてください。
placeHolderぽくするためにtextViewの文字色をlightgrayにしました。

func textViewDidBeginEditing(_ textView: UITextView) {
        textView.text = ""
        textView.textColor = .black
}

textViewDidBeginEditingで、textViewがタップされた時の挙動を表します。
つまり、「textViewがタップされた時、textViewの文字を無くして、色を黒にする」
という処理を行なっています。

func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.text == "" {
            textView.text = "内容を入力"
            textView.textColor = .lightGray
        }
}

ここは、placeHolderを実装するだけならいらない部分なのかもしれないのですが、
textViewに何も文字が書いていない場合のtextViewの処理を表しています。

まとめ

う〜ん、やっぱりtextViewにもplaceHolderは欲しいですね〜
冒頭にも書きましたが、かなり自己流ですが、一応placeHolder的なものはできました。
たくさんの方の役に立つと嬉しいです。
質問がある方は、コメント欄まで〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Modalで表示した子画面の閉じるイベントを親画面で検知する

概要

iOS13からModalで画面遷移した際のアニメーションが変更されました。
それによって、Modal画面を閉じた際に動くライフサイクルメソッドが変わりました。
(今まではViewWillAppearが動いていたので、そこで更新等の処理を書いていた人も多いはず。困った、、、)

Modalで表示した子画面の閉じるイベントを親画面で検知する方法を備忘として残しておきます。

環境

Xcode: 11.4
Swift: Swift5

手順1

適当なViewControllerを2つ用意。
ここでは遷移元を「ParentViewController」遷移先を「ChildViewController」とします。

ParentViewController.swift
import UIKit

final class ParentViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
  }
}
ChildViewController.swift
import UIKit

final class ChildViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
  }
}

手順2

ChildViewControllerが閉じた際のイベントは以下のメソッドで検知可能です。

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController

「UIAdaptivePresentationControllerDelegate」に準拠することで使用可能です。
ParentViewControllerに以下を実装します。

ParentViewController.swift
import UIKit

final class ParentViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    let storyboard = UIStoryboard.init(name: "Child", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "AccountViewController")
    // delegateを設定
    vc.presentationController?.delegate = self
    self.present(vc, animated: true, completion: nil)
  }
}

extension ParentViewController: UIAdaptivePresentationControllerDelegate {
  func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
    // ChildViewControllerのDismissを検知
    print("closed")
  }
}

最後に

iOS13が出てから半年以上経っているのに調べるまで知りませんでした。
ライフサイクルメソッドに書かなくていいので実装がスッキリしますね!
誰かのお役に立てれば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像ファイルの種類を判別するDataのExtension

Swiftでファイルを取扱う際、URLであればpathExtensionで拡張子を取得することができます。しかしDataからファイルの種類を特定する場合、基本機能だけでは不可能です。NSDataから判別する記事を書いている方がいたので、これを元にDataから画像の種別を特定するExtensionを作成しました。

コード全貌 (+解説)

extension Data {
    enum ImageType: String, CaseIterable {
        case jpg
        case png
        case gif
        case bmp

        var header: UInt8 {
            switch self {
            case .jpg:
                return 0xff

            case .png:
                return 0x89

            case .gif:
                return 0x47

            case .bmp:
                return 0x42
            }
        }

        var `extension`: String {
            return "." + rawValue
        }
    }

    var imageType: ImageType? {
        var values = [UInt8](repeating: 0, count: 1)
        copyBytes(to: &values, count: 1)
        return ImageType.allCases.first { $0.header == values[0] }
    }
}

画像ファイルの種別はバイナリデータのヘッダを見ればわかる(らしい)ので、その情報を持つenum ImageTypeを定義しておき、var imageType内で実際のヘッダとの比較を行っています。判別する画像の種類を増やしたいときはenum ImageTypecaseを追加すればよいです。楽ですね。

まとめ

正直バイナリとか触ったことなくて不安しかないので、間違い等あればご指摘お願いします...

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む