2016 Swift Advent Calenderの21日目です。
今回はアプリ開発をする時にはほとんどの場面で作るであろうユーザー登録機能をどのように実装していくのかという観点から役に立ちそうなProtocolを活用したDesign Patternをまとめていきます。(基礎的な内容であろう事項をまとめるので応用的・実践的なPattern等はコメントにて教えていただけると嬉しいです。)
今回考えるのは以下のようなUI達です。
アプリの初回立ち上げ時に新規ユーザー登録を行う際はUserのモデルクラスを作って、そこにデータを管理することになると思います。
そのためにアプリ内で一つしか存在しえないインスタンスを生成してデータ管理をすることになるでしょう。その際には言わずもがなSingleton Patternを採用することになります。
1. Singleton Pattern
シングルトンを作る際にはSwiftではstaticを活用すると良さそうです。
struct UserSettingClass {
static var sharedInstance = UserSettingClass()
static var email: String = ""
static var password: String = ""
static var userName: String = ""
static var gender: Gender = .unknown
static var birthday: Date? = nil
static var profileImage: Data? = nil
enum Gender {
case unknown
case man
case woman
}
static func saveData(){
//処理
}
}
そうすると以下の様にシングルトンを生成できます。
var userSetting = UserSettingClass.sharedInstance
しかし、class
とstruct
は空の状態でもインスタンスの初期化が出来てしまいますが、空のenum
では出来ません。そのためにenum
を活用するという手もありそうです。
enum UserSettingEnum {
static var email: String = ""
static var password: String = ""
static var userName: String = ""
static var gender: Gender = .unknown
static var birthday: Date? = nil
static var profileImage: Data? = nil
enum Gender {
case unknown
case man
case woman
}
static func saveData(){
//処理
}
}
これで以下の様に呼び出しが出来ます。
var textField: UITextField!
UserSettingEnum.email = textField.text!
2. Builder Pattern
今回のUserSettingのモデルはプロパティ数が少なく初期化特に苦労はしなさそうです。
しかし、場合によってはUserモデルが大きくなりプロパティ数が多くなりすぎることがありえます。
class ComplexityUser {
var name: String?
var age: Int?
var email: String?
var password: String?
var prefecture: String?
var job: String?
var weight: Double?
var height: Double?
var message: String?
init(name: String, age: Int, email: String, password: String, prefecture: String, weight: Double, height: Double, message: String) {
self.name = name
self.age = age
self.email = email
self.password = password
self.prefecture = prefecture
self.weight = weight
self.height = height
self.message = message
}
}
この場合、初期化のコードは以下の様になります。
let complexityUser = ComplexityUser(name: "aritaku", age: 25, email: "hoge@hoge", password: "password", prefecture: "shiga", weight: 62.0, height: 180.0, message: "hello")
辛いですね。そういった場合ではBuilder Patternが使えそうです。
protocol UserBuilder {
var name: String { get }
var age: Int { get }
var email: String { get }
var password: String { get }
var prefecture: String { get }
var job: String { get }
var weight: Double { get }
var height: Double { get }
var message: String { get }
}
struct AppUserBuilder: UserBuilder {
var name = "Burger"
var age = 25
var email = "hoge@hoge"
var password = "hogehogehoge"
var prefecture = "Kagoshima"
var job = "Swift Programmer"
var weight = 62.2
var height = 180.0
var message = "はじめまして。"
}
struct User {
var name: String
var age: Int
var email: String
var password: String
var prefecture: String
var job: String
var weight: Double
var height: Double
var message: String
init(builder: UserBuilder) {
self.name = builder.name
self.age = builder.age
self.email = builder.email
self.password = builder.password
self.prefecture = builder.prefecture
self.job = builder.job
self.weight = builder.weight
self.height = builder.height
self.message = builder.message
}
}
最初にUserBuilder
というprotocol
を宣言してあります。
このUserBuilder Protocol
を活用してUserモデルの初期化をしてあげるというアイデアです。
すると初期化は以下の様に楽に済ませることができます。
var newUser = User(builder: AppUserBuilder())
3. Factory Pattern
次に以下の画面のようにパスワードやEmailなどが入力されたときにバリデーションをかける場合を考えます。
めんどくさいのは、入力された値の種類によってどのようなバリデーションをかけるのかが変わってくる時です。
その場合にはFactory Patternが有効です。
以下の様にValidationに関するProtocolを宣言します。
protocol TextValidationProtocol {
var regExFindMatchString: String { get }
var validationMessage: String { get }
}
extension TextValidationProtocol {
var regExMatchingString: String {
get {
return regExFindMatchString + "$"
}
}
func validationString(str: String) -> Bool {
if let _ = str.range(of: regExMatchingString, options: .regularExpression) {
return true
} else {
return false
}
}
func getMatchingString(str: String) -> String? {
if let newMatch = str.range(of: regExFindMatchString, options: .regularExpression) {
return str.substring(with: newMatch)
} else {
return nil
}
}
}
次に想定されるバリデーションの種類をTextValidationProtocol
にもとづいて宣言していきます。
今回のシングルトンはenumではstored propertyが持てないためにclassを使用しました。
class AlphaValidation: TextValidationProtocol {
static let sharedInstance = AlphaValidation()
private init(){}
let regExFindMatchString = "^[a-zA-Z]{0,10}"
let validationMessage = "Can only contain Alpha characters"
}
class AlphaNumericValidation: TextValidationProtocol {
static let sharedInstance = AlphaNumericValidation()
private init(){}
let regExFindMatchString = "^[a-zA-Z0-9]{0,10}"
let validationMessage = "Can only contain Alpha Numeric characters"
}
class NumericValidation: TextValidationProtocol {
static let sharedInstance = NumericValidation()
private init(){}
let regExFindMatchString = "^[0-9]{0,10}"
let validationMessage = "Can only contain only Alpha and Numeric characters."
}
また以上の種類のバリデーション操作をまとめたメソッドを作成します。
引数にalphaCharacters
とnumericCharacters
のBool値を持たせることによってかけるべきバリデーション種類を決めます。
func getValidator(alphaCharacters: Bool, numericCharacters: Bool) -> TextValidationProtocol? {
if alphaCharacters && numericCharacters {
return AlphaNumericValidation.sharedInstance
} else if alphaCharacters && !numericCharacters {
return AlphaValidation.sharedInstance
} else if !alphaCharacters && numericCharacters {
return NumericValidation.sharedInstance
} else {
return nil
}
}
そうすることで以下の様にバリデーションをかけることができます。
var str = "abc123"
var validator1 = getValidator(alphaCharacters: true, numericCharacters: true)
print("String Validated: \(validator1?.validationString(str: str))") //true
var validator2 = getValidator(alphaCharacters: true, numericCharacters: false)
print("String Validated: \(validator2?.validationString(str: str))") //false
簡単ではありましたが、以上でユーザー登録機能の実装の際に役に立ちそうなProtocolを活用したDesign Pattern3選の紹介を終わります。シングルトンのところはProtocol使ってないじゃんっていうツッコミは心の中にとどめておいてください。笑
気になるところが色々とご指摘いただければ嬉しいです。