├── MvvmDemo ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── DataDrivenDemo │ ├── Resources │ │ └── avatar.jpg │ ├── LoginViewController.h │ ├── LoginViewModel.h │ ├── LoginViewController.m │ ├── LoginView.h │ ├── LoginViewModel.m │ └── LoginView.m ├── ViewController.h ├── AppDelegate.h ├── SceneDelegate.h ├── MVCDemo │ ├── MVCLoginViewController.h │ ├── MVCLoginView.h │ ├── MVCLoginViewController.m │ └── MVCLoginView.m ├── main.m ├── DataDriven │ ├── Base │ │ ├── Observable.h │ │ ├── Observer.h │ │ ├── Observable.m │ │ └── Observer.m │ └── Combine │ │ ├── ObservableCombiner.h │ │ └── ObservableCombiner.m ├── AppDelegate.m ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard ├── ViewController.m ├── Info.plist └── SceneDelegate.m ├── README.md ├── MvvmDemo.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── Troyan.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── yanyongcai.xcuserdatad │ │ │ ├── UserInterfaceState.xcuserstate │ │ │ └── IDEFindNavigatorScopes.plist │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ ├── yanyongcai.xcuserdatad │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── Troyan.xcuserdatad │ │ ├── xcschemes │ │ └── xcschememanagement.plist │ │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist └── project.pbxproj ├── MvvmDemoTests ├── Info.plist └── DataDrivenDemoTests │ ├── LoginViewTests.m │ └── LoginViewModelTests.m └── MvvmDemoUITests ├── Info.plist └── MvvmDemoUITests.m /MvvmDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/Resources/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luminixus/DataDrivenMVVM/HEAD/MvvmDemo/DataDrivenDemo/Resources/avatar.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataDrivenMVVM 2 | 3 | A light-weighted data driven mvvm framework. 4 | 5 | [实现一套轻量级MVVM框架](https://juejin.cn/post/6947583052377751560) 6 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MvvmDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/xcuserdata/yanyongcai.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.xcworkspace/xcuserdata/Troyan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luminixus/DataDrivenMVVM/HEAD/MvvmDemo.xcodeproj/project.xcworkspace/xcuserdata/Troyan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MvvmDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import 9 | 10 | @interface ViewController : UIViewController 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.xcworkspace/xcuserdata/yanyongcai.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luminixus/DataDrivenMVVM/HEAD/MvvmDemo.xcodeproj/project.xcworkspace/xcuserdata/yanyongcai.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MvvmDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.xcworkspace/xcuserdata/yanyongcai.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MvvmDemo/SceneDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import 9 | 10 | @interface SceneDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow * window; 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface LoginViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /MvvmDemo/MVCDemo/MVCLoginViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MVCViewController.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface MVCLoginViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/xcuserdata/Troyan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MvvmDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/xcuserdata/yanyongcai.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MvvmDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MvvmDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import 9 | #import "AppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | NSString * appDelegateClassName; 13 | @autoreleasepool { 14 | // Setup code that might create autoreleased objects goes here. 15 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 16 | } 17 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 18 | } 19 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/xcuserdata/Troyan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Base/Observable.h: -------------------------------------------------------------------------------- 1 | // 2 | // Observable.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @protocol Observer; 13 | /// 可观察对象,value 成员更新 setter 会驱动注册的观察者刷新。注册观察者后,观察者被可观察对象强持有 14 | @protocol Observable 15 | 16 | /// 值 17 | @property(strong, nonatomic, nullable) id value; 18 | 19 | /// 添加观察者 20 | -(void)addObserver:(id)observer; 21 | 22 | /// 移除观察者 23 | -(void)removeObserver:(id)observer; 24 | 25 | @end 26 | 27 | 28 | /// 可观察对象 29 | @interface Observable : NSObject 30 | 31 | /// 构建 32 | @property(copy, nonatomic, class, readonly) Observable *(^create)(id _Nullable defaultValue); 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /MvvmDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MvvmDemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Base/Observer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Observer.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @protocol Observable; 13 | /// 观察者 14 | @protocol Observer 15 | 16 | /// 订阅可观察对象 17 | @property(copy, nonatomic, readonly) void (^subscribe)(id observable); 18 | 19 | /// 触发值刷新 20 | -(void)invoke:(id)newValue; 21 | 22 | @end 23 | 24 | /// 观察者所注册的操作 25 | typedef void(^ObserverHandler)(id newValue); 26 | 27 | 28 | /// 观察者 29 | @interface Observer : NSObject 30 | 31 | /// 构建 32 | @property(copy, nonatomic, class, readonly) Observer *(^create)(void); 33 | 34 | /// 处理值刷新 35 | @property(copy, nonatomic, readonly) Observer * (^handle)(ObserverHandler); 36 | 37 | @end 38 | 39 | NS_ASSUME_NONNULL_END 40 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "Observable.h" 9 | #import "ObservableCombiner.h" 10 | #import "Observer.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface LoginViewModel : NSObject 15 | 16 | //MARK: 数据驱动UI刷新 17 | @property(strong, nonatomic) Observable *username; 18 | 19 | @property(strong, nonatomic) Observable *password; 20 | 21 | @property(strong, nonatomic) ObservableCombiner *usernameValid; 22 | 23 | @property(strong, nonatomic) ObservableCombiner *passwordValid; 24 | 25 | @property(strong, nonatomic) ObservableCombiner *instruction; 26 | 27 | @property(strong, nonatomic) ObservableCombiner *loginEnabled; 28 | 29 | //MARK: 用户交互动作订阅 30 | @property(strong, nonatomic) Observer *usernameDidChange; 31 | 32 | @property(strong, nonatomic) Observer *passwordDidChange; 33 | 34 | @property(strong, nonatomic) Observer *loginTouched; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Combine/ObservableCombiner.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableCombiner.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import "Observable.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | /// 合成可观察对象的触发策略 13 | typedef NS_ENUM(NSUInteger, CombineStrategy) { 14 | /// 第一次值更新才刷新 15 | CombineStrategyFirst, 16 | /// 所有值更新才刷新 17 | CombineStrategyAll, 18 | /// 任何值更新均刷新 19 | CombineStrategyEvery, 20 | }; 21 | 22 | /// 合成可观察对象处理值刷新 23 | typedef _Nullable id (^CombinerHandler)(NSArray *newValues); 24 | 25 | /// 合成多个可观察对象 26 | @interface ObservableCombiner : Observable 27 | 28 | /// 安全获取值 29 | @property(copy, nonatomic, readonly, class) id _Nullable (^safeValue)(NSArray *, NSInteger); 30 | 31 | /// 构建 32 | #pragma clang diagnostic push 33 | #pragma clang diagnostic ignored "-Wincompatible-property-type" 34 | @property(copy, nonatomic, readonly, class) ObservableCombiner *(^create)(CombineStrategy strategy); 35 | #pragma clang diagnostic pop 36 | 37 | /// 合并可观察对象 38 | @property(copy, nonatomic, readonly) ObservableCombiner * (^combine)(id observable); 39 | 40 | /// 处理值刷新 41 | @property(copy, nonatomic, readonly) ObservableCombiner * (^handle)(CombinerHandler); 42 | 43 | @end 44 | 45 | NS_ASSUME_NONNULL_END 46 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Base/Observable.m: -------------------------------------------------------------------------------- 1 | // 2 | // Observable.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import "Observable.h" 9 | #import "Observer.h" 10 | 11 | @interface Observable () 12 | 13 | @property(strong, nonatomic) NSMutableArray *observers; 14 | 15 | @end 16 | 17 | @implementation Observable 18 | 19 | @synthesize value = _value; 20 | 21 | -(void)setValue:(id)value{ 22 | if(![self.value isEqual:value]){ 23 | _value = value; 24 | for(id observer in self.observers){ 25 | [observer invoke:value]; 26 | } 27 | } 28 | } 29 | 30 | static Observable *(^create)(id) = ^Observable *(id defaultValue){ 31 | Observable *observable = [[Observable alloc] init]; 32 | observable.value = defaultValue; 33 | return observable; 34 | }; 35 | 36 | +(Observable *(^)(id))create{ 37 | return create; 38 | } 39 | 40 | -(void)addObserver:(id)observer{ 41 | [self.observers addObject:observer]; 42 | } 43 | 44 | -(void)removeObserver:(id)observer{ 45 | [self.observers removeObject:observer]; 46 | } 47 | 48 | -(NSMutableArray *)observers{ 49 | if(!_observers){ 50 | _observers = [[NSMutableArray alloc] init]; 51 | } 52 | return _observers; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /MvvmDemo/MVCDemo/MVCLoginView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import 9 | 10 | #define LightRed [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xCC/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 11 | #define LightGreen [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xFF/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 12 | #define LightBlue [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 13 | #define LightYellow [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xFF/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 14 | #define LightMagenta [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 15 | #define LightCyan [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xFF/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 16 | #define LightGray [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 17 | #define ThemeColor [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0x37/0xFF blue:1.0*0x00/0xFF alpha:1.0] 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | @interface MVCLoginView : UIView 22 | 23 | @property(strong, nonatomic) UITextField *usernameTextField; 24 | 25 | @property(strong, nonatomic) UITextField *passwordTextField; 26 | 27 | @property(strong, nonatomic) UILabel *instructionLabel; 28 | 29 | @property(strong, nonatomic) UIButton *loginButton; 30 | 31 | @end 32 | 33 | NS_ASSUME_NONNULL_END 34 | -------------------------------------------------------------------------------- /MvvmDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import "AppDelegate.h" 9 | 10 | @interface AppDelegate () 11 | 12 | @end 13 | 14 | @implementation AppDelegate 15 | 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 18 | // Override point for customization after application launch. 19 | return YES; 20 | } 21 | 22 | 23 | #pragma mark - UISceneSession lifecycle 24 | 25 | 26 | - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; 30 | } 31 | 32 | 33 | - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { 34 | // Called when the user discards a scene session. 35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 37 | } 38 | 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Base/Observer.m: -------------------------------------------------------------------------------- 1 | // 2 | // Observer.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import "Observer.h" 9 | #import "Observable.h" 10 | 11 | @interface Observer () 12 | 13 | @property(copy, nonatomic) Observer * (^handle)(ObserverHandler); 14 | 15 | @property(copy, nonatomic) ObserverHandler handler; 16 | 17 | @end 18 | 19 | @implementation Observer 20 | 21 | @synthesize subscribe = _subscribe; 22 | 23 | -(void (^)(id))subscribe{ 24 | if(!_subscribe){ 25 | __weak typeof(self) weakSelf = self; 26 | _subscribe = ^(id observable){ 27 | __strong typeof(weakSelf) strongSelf = weakSelf; 28 | [observable addObserver:strongSelf]; 29 | }; 30 | } 31 | return _subscribe; 32 | } 33 | 34 | -(void)invoke:(id)newValue{ 35 | if(self.handler){ 36 | self.handler(newValue); 37 | } 38 | } 39 | 40 | static Observer *(^create)(void) = ^Observer *(){ 41 | Observer *observer = [[Observer alloc] init]; 42 | return observer; 43 | }; 44 | 45 | +(Observer *(^)(void))create{ 46 | return create; 47 | } 48 | 49 | -(Observer * (^)(ObserverHandler))handle{ 50 | if(!_handle){ 51 | __weak typeof(self) weakSelf = self; 52 | _handle = ^Observer *(ObserverHandler handler){ 53 | __strong typeof(weakSelf) strongSelf = weakSelf; 54 | strongSelf.handler = handler; 55 | return strongSelf; 56 | }; 57 | } 58 | return _handle; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /MvvmDemoUITests/MvvmDemoUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MvvmDemoUITests.m 3 | // MvvmDemoUITests 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import 9 | 10 | @interface MvvmDemoUITests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation MvvmDemoUITests 15 | 16 | - (void)setUp { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | 19 | // In UI tests it is usually best to stop immediately when a failure occurs. 20 | self.continueAfterFailure = NO; 21 | 22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 23 | } 24 | 25 | - (void)tearDown { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | - (void)testExample { 30 | // UI tests must launch the application that they test. 31 | XCUIApplication *app = [[XCUIApplication alloc] init]; 32 | [app launch]; 33 | 34 | // Use recording to get started writing UI tests. 35 | // Use XCTAssert and related functions to verify your tests produce the correct results. 36 | } 37 | 38 | - (void)testLaunchPerformance { 39 | if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { 40 | // This measures how long it takes to launch your application. 41 | [self measureWithMetrics:@[[[XCTApplicationLaunchMetric alloc] init]] block:^{ 42 | [[[XCUIApplication alloc] init] launch]; 43 | }]; 44 | } 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /MvvmDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MvvmDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "LoginViewController.h" 9 | #import "LoginView.h" 10 | #import "LoginViewModel.h" 11 | #import "Observer.h" 12 | 13 | @interface LoginViewController () 14 | 15 | @property(strong, nonatomic) LoginView *loginView; 16 | 17 | @property(strong, nonatomic) LoginViewModel *loginViewModel; 18 | 19 | @end 20 | 21 | @implementation LoginViewController 22 | 23 | - (void)viewDidLoad { 24 | [super viewDidLoad]; 25 | 26 | self.loginView = [[LoginView alloc] initWithFrame:UIScreen.mainScreen.bounds]; 27 | [self.view addSubview:self.loginView]; 28 | 29 | self.loginViewModel = [[LoginViewModel alloc] init]; 30 | 31 | [self doDataBindings]; 32 | 33 | [self doActionBindings]; 34 | 35 | [self initStates]; 36 | } 37 | 38 | /// 数据驱动UI刷新 39 | -(void)doDataBindings{ 40 | self.loginView.username.subscribe(self.loginViewModel.username); 41 | 42 | self.loginView.password.subscribe(self.loginViewModel.password); 43 | 44 | self.loginView.instruction.subscribe(self.loginViewModel.instruction); 45 | 46 | self.loginView.usernameValid.subscribe(self.loginViewModel.usernameValid); 47 | 48 | self.loginView.passwordValid.subscribe(self.loginViewModel.passwordValid); 49 | 50 | self.loginView.loginEnabled.subscribe(self.loginViewModel.loginEnabled); 51 | } 52 | 53 | /// 用户交互动作订阅 54 | -(void)doActionBindings{ 55 | self.loginViewModel.usernameDidChange.subscribe(self.loginView.usernameDidChange); 56 | 57 | self.loginViewModel.passwordDidChange.subscribe(self.loginView.passwordDidChange); 58 | 59 | self.loginViewModel.loginTouched.subscribe(self.loginView.loginButtonTouched); 60 | } 61 | 62 | /// 状态初始化 63 | -(void)initStates{ 64 | self.loginViewModel.username.value = @"maruko"; 65 | self.loginViewModel.password.value = @"1234"; 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /MvvmDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginView.h: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.h 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import 9 | #import "Observable.h" 10 | #import "Observer.h" 11 | 12 | #define LightRed [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xCC/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 13 | #define LightGreen [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xFF/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 14 | #define LightBlue [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 15 | #define LightYellow [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xFF/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 16 | #define LightMagenta [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 17 | #define LightCyan [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xFF/0xFF blue:1.0*0xFF/0xFF alpha:1.0] 18 | #define LightGray [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xCC/0xFF alpha:1.0] 19 | #define ThemeColor [UIColor colorWithRed:1.0*0xFF/0xFF green:1.0*0x37/0xFF blue:1.0*0x00/0xFF alpha:1.0] 20 | 21 | NS_ASSUME_NONNULL_BEGIN 22 | 23 | @interface LoginView : UIView 24 | 25 | @property(strong, nonatomic) UITextField *usernameTextField; 26 | 27 | @property(strong, nonatomic) UITextField *passwordTextField; 28 | 29 | @property(strong, nonatomic) UILabel *instructionLabel; 30 | 31 | @property(strong, nonatomic) UIButton *loginButton; 32 | 33 | //MARK: 数据驱动UI刷新 34 | @property(strong, nonatomic) Observer *username; 35 | 36 | @property(strong, nonatomic) Observer *password; 37 | 38 | @property(strong, nonatomic) Observer *instruction; 39 | 40 | @property(strong, nonatomic) Observer *usernameValid; 41 | 42 | @property(strong, nonatomic) Observer *passwordValid; 43 | 44 | @property(strong, nonatomic) Observer *loginEnabled; 45 | 46 | //MARK: 用户交互动作订阅 47 | @property(strong, nonatomic) Observable *usernameDidChange; 48 | 49 | @property(strong, nonatomic) Observable *passwordDidChange; 50 | 51 | @property(strong, nonatomic) Observable *loginButtonTouched; 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /MvvmDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import "ViewController.h" 9 | #import "Observable.h" 10 | #import "Observer.h" 11 | #import "ObservableCombiner.h" 12 | #import "LoginViewController.h" 13 | #import "MVCLoginViewController.h" 14 | 15 | @interface ViewController () 16 | 17 | @property(strong, nonatomic) Observable *observable1; 18 | 19 | @property(strong, nonatomic) Observable *observable2; 20 | 21 | @property(strong, nonatomic) ObservableCombiner *observableCombiner; 22 | 23 | @end 24 | 25 | @implementation ViewController 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | 30 | // self.observable1 = [[Observable alloc] init]; 31 | // self.observable2 = [[Observable alloc] init]; 32 | // self.observableCombiner = ObservableCombiner.create(CombineStrategyAll) 33 | // .combine(self.observable1) 34 | // .combine(self.observable2) 35 | // .handle(^id _Nullable(NSArray * _Nonnull newValues) { 36 | // return [NSString stringWithFormat:@"(%@, %@)", newValues[0], newValues[1]]; 37 | // }); 38 | // 39 | // Observer.create().handle(^(id newValue) { 40 | // NSLog(@"block new value: %@", newValue); 41 | // }).subscribe(self.observable1); 42 | // 43 | // Observer.create().handle(^(id newValue) { 44 | // NSLog(@"combined new value: %@", newValue); 45 | // }).subscribe(self.observableCombiner); 46 | // 47 | // self.observable1.value = @"咔咔咔"; 48 | // self.observable2.value = @"嘻嘻嘻"; 49 | 50 | LoginViewController *vc = [[LoginViewController alloc] init]; 51 | [self addChildViewController:vc]; 52 | [self.view addSubview:vc.view]; 53 | vc.view.frame = UIScreen.mainScreen.bounds; 54 | 55 | // MVCLoginViewController *vc = [[MVCLoginViewController alloc] init]; 56 | // [self addChildViewController:vc]; 57 | // [self.view addSubview:vc.view]; 58 | // vc.view.frame = UIScreen.mainScreen.bounds; 59 | } 60 | 61 | //-(void)handle:(id)newValue{ 62 | // NSLog(@"target action new value: %@", newValue); 63 | //} 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /MvvmDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /MvvmDemo/SceneDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/3/14. 6 | // 7 | 8 | #import "SceneDelegate.h" 9 | 10 | @interface SceneDelegate () 11 | 12 | @end 13 | 14 | @implementation SceneDelegate 15 | 16 | 17 | - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | } 22 | 23 | 24 | - (void)sceneDidDisconnect:(UIScene *)scene { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | 32 | - (void)sceneDidBecomeActive:(UIScene *)scene { 33 | // Called when the scene has moved from an inactive state to an active state. 34 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 35 | } 36 | 37 | 38 | - (void)sceneWillResignActive:(UIScene *)scene { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | 44 | - (void)sceneWillEnterForeground:(UIScene *)scene { 45 | // Called as the scene transitions from the background to the foreground. 46 | // Use this method to undo the changes made on entering the background. 47 | } 48 | 49 | 50 | - (void)sceneDidEnterBackground:(UIScene *)scene { 51 | // Called as the scene transitions from the foreground to the background. 52 | // Use this method to save data, release shared resources, and store enough scene-specific state information 53 | // to restore the scene back to its current state. 54 | } 55 | 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /MvvmDemoTests/DataDrivenDemoTests/LoginViewTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewTests.m 3 | // MvvmDemoTests 4 | // 5 | // Created by Luminixus on 2021/5/18. 6 | // 7 | 8 | #import 9 | #import "LoginView.h" 10 | 11 | @interface LoginViewTests : XCTestCase 12 | 13 | @property(strong, nonatomic) LoginView *loginView; 14 | 15 | @end 16 | 17 | @implementation LoginViewTests 18 | 19 | - (void)setUp { 20 | self.loginView = [[LoginView alloc] init]; 21 | } 22 | 23 | - (void)tearDown { 24 | self.loginView = nil; 25 | } 26 | 27 | - (void)testUsername { 28 | Observable *username = Observable.create(nil); 29 | self.loginView.username.subscribe(username); 30 | 31 | username.value = @"maruko"; 32 | 33 | XCTAssertTrue([@"maruko" isEqualToString:self.loginView.usernameTextField.text]); 34 | } 35 | 36 | - (void)testPassword { 37 | Observable *pwd = Observable.create(nil); 38 | self.loginView.password.subscribe(pwd); 39 | 40 | pwd.value = @"123456"; 41 | 42 | XCTAssertTrue([@"123456" isEqualToString:self.loginView.passwordTextField.text]); 43 | } 44 | 45 | - (void)testUsernameValid_whenUsernameInvalid_backgroundLightRed { 46 | Observable *usernameValid = Observable.create(nil); 47 | self.loginView.usernameValid.subscribe(usernameValid); 48 | 49 | usernameValid.value = @(NO); 50 | 51 | CGFloat lightRedR, lightRedG, lightRedB, lightRedA; 52 | [LightRed getRed:&lightRedR green:&lightRedG blue:&lightRedB alpha:&lightRedA]; 53 | CGFloat bgR, bgG, bgB, bgA; 54 | [self.loginView.usernameTextField.backgroundColor getRed:&bgR green:&bgG blue:&bgB alpha:&bgA]; 55 | XCTAssertEqual(lightRedR, bgR); 56 | XCTAssertEqual(lightRedG, bgG); 57 | XCTAssertEqual(lightRedB, bgB); 58 | XCTAssertEqual(lightRedA, bgA); 59 | } 60 | 61 | - (void)testPasswordValid_whenPasswordInvalid_backgroundLightRed { 62 | Observable *passwordValid = Observable.create(nil); 63 | self.loginView.passwordValid.subscribe(passwordValid); 64 | 65 | passwordValid.value = @(NO); 66 | 67 | CGFloat lightRedR, lightRedG, lightRedB, lightRedA; 68 | [LightRed getRed:&lightRedR green:&lightRedG blue:&lightRedB alpha:&lightRedA]; 69 | CGFloat bgR, bgG, bgB, bgA; 70 | [self.loginView.passwordTextField.backgroundColor getRed:&bgR green:&bgG blue:&bgB alpha:&bgA]; 71 | XCTAssertEqual(lightRedR, bgR); 72 | XCTAssertEqual(lightRedG, bgG); 73 | XCTAssertEqual(lightRedB, bgB); 74 | XCTAssertEqual(lightRedA, bgA); 75 | } 76 | 77 | // And So On 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /MvvmDemoTests/DataDrivenDemoTests/LoginViewModelTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModelTests.m 3 | // MvvmDemoTests 4 | // 5 | // Created by Luminixus on 2021/5/18. 6 | // 7 | 8 | #import 9 | #import "LoginViewModel.h" 10 | 11 | @interface LoginViewModelTests : XCTestCase 12 | 13 | @property(strong, nonatomic) LoginViewModel *viewModel; 14 | 15 | @end 16 | 17 | @implementation LoginViewModelTests 18 | 19 | - (void)setUp { 20 | self.viewModel = [[LoginViewModel alloc] init]; 21 | } 22 | 23 | - (void)tearDown { 24 | self.viewModel = nil; 25 | } 26 | 27 | - (void)testInit { 28 | XCTAssertNotNil(self.viewModel.username); 29 | XCTAssertNotNil(self.viewModel.password); 30 | XCTAssertNotNil(self.viewModel.usernameValid); 31 | XCTAssertNotNil(self.viewModel.passwordValid); 32 | XCTAssertNotNil(self.viewModel.instruction); 33 | XCTAssertNotNil(self.viewModel.loginEnabled); 34 | } 35 | 36 | - (void)testUsername { 37 | NSString *maruko = @"Maruko"; 38 | 39 | Observer.create().handle(^(id _Nonnull newValue) { 40 | XCTAssert([newValue isEqualToString:maruko]); 41 | }).subscribe(self.viewModel.username); 42 | 43 | self.viewModel.username.value = maruko; 44 | } 45 | 46 | - (void)testPassword { 47 | NSString *pwd = @"123456"; 48 | 49 | Observer.create().handle(^(id _Nonnull newValue) { 50 | XCTAssert([newValue isEqualToString:pwd]); 51 | }).subscribe(self.viewModel.password); 52 | 53 | self.viewModel.password.value = pwd; 54 | } 55 | 56 | - (void)testUsernameValid_usernameCantBeShorterThan6 { 57 | NSString *shortName = @"Tsu"; 58 | 59 | Observer.create().handle(^(id _Nonnull newValue) { 60 | XCTAssertFalse([newValue boolValue]); 61 | }).subscribe(self.viewModel.usernameValid); 62 | 63 | self.viewModel.username.value = shortName; 64 | } 65 | 66 | - (void)testUsernameValid_usernameCantBeLongerThan24 { 67 | NSString *longName = @"Tsu01234567890123456789Tsu"; 68 | 69 | Observer.create().handle(^(id _Nonnull newValue) { 70 | XCTAssertFalse([newValue boolValue]); 71 | }).subscribe(self.viewModel.usernameValid); 72 | 73 | self.viewModel.username.value = longName; 74 | } 75 | 76 | - (void)testUsernameDidChange_usernameChangeSynchronously { 77 | Observable *action = Observable.create(nil); 78 | self.viewModel.usernameDidChange.subscribe(action); 79 | 80 | action.value = @"Maruko"; 81 | 82 | XCTAssertTrue([@"Maruko" isEqualToString:self.viewModel.username.value]); 83 | } 84 | 85 | - (void)testLoginTouched_instructionShowSuccess { 86 | Observable *action = Observable.create(nil); 87 | self.viewModel.loginTouched.subscribe(action); 88 | 89 | action.value = nil; 90 | 91 | XCTAssertTrue([@"登录成功(*^▽^*)" isEqualToString:self.viewModel.instruction.value]); 92 | } 93 | 94 | // And So On 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginViewModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "LoginViewModel.h" 9 | 10 | #define WS __weak typeof(self) weakSelf = self; 11 | #define SS __strong typeof(weakSelf) self = weakSelf; 12 | 13 | @implementation LoginViewModel 14 | 15 | -(instancetype)init{ 16 | if(self = [super init]){ 17 | self.username = Observable.create(nil); 18 | self.password = Observable.create(nil); 19 | 20 | self.usernameValid = ObservableCombiner.create(CombineStrategyEvery); 21 | self.passwordValid = ObservableCombiner.create(CombineStrategyEvery); 22 | self.instruction = ObservableCombiner.create(CombineStrategyEvery); 23 | self.loginEnabled = ObservableCombiner.create(CombineStrategyEvery); 24 | 25 | [self doDataWeaving]; 26 | 27 | [self doActionProcessing]; 28 | } 29 | return self; 30 | } 31 | 32 | -(void)doDataWeaving{ 33 | //NOTE: 数据组织时尽量不要在handle中调用view model自身,尽量用传入handle的参数值去驱动 34 | self.usernameValid 35 | .combine(self.username) 36 | .handle(^id _Nullable(NSArray * _Nonnull newValues) { 37 | NSString *username = ObservableCombiner.safeValue(newValues, 0); 38 | return @(username.length && username.length >= 6 && username.length <= 24); 39 | }); 40 | 41 | self.passwordValid 42 | .combine(self.password) 43 | .handle(^id _Nullable(NSArray * _Nonnull newValues) { 44 | NSString *password = ObservableCombiner.safeValue(newValues, 0); 45 | return @(password.length && password.length >= 6 && password.length <= 24); 46 | }); 47 | 48 | self.instruction 49 | .combine(self.username) 50 | .combine(self.password) 51 | .handle(^id _Nullable(NSArray * _Nonnull newValues) { 52 | NSString *username = ObservableCombiner.safeValue(newValues, 0); 53 | if(!username.length){ 54 | return @"*用户名不能为空"; 55 | } 56 | if(username.length < 6){ 57 | return @"*用户名必须超过6个字符"; 58 | } 59 | if(username.length > 24){ 60 | return @"*用户名不能超过24个字符"; 61 | } 62 | 63 | NSString *password = ObservableCombiner.safeValue(newValues, 1); 64 | if(!password.length){ 65 | return @"*密码不能为空"; 66 | } 67 | if(password.length < 6){ 68 | return @"*密码必须超过6个字符"; 69 | } 70 | if(password.length > 24){ 71 | return @"*密码不能超过24个字符"; 72 | } 73 | 74 | return @""; 75 | }); 76 | 77 | self.loginEnabled 78 | .combine(self.usernameValid) 79 | .combine(self.passwordValid) 80 | .handle(^id _Nullable(NSArray * _Nonnull newValues) { 81 | BOOL usernameValid = [ObservableCombiner.safeValue(newValues, 0) boolValue]; 82 | BOOL passworkValid = [ObservableCombiner.safeValue(newValues, 1) boolValue]; 83 | return @(usernameValid && passworkValid); 84 | }); 85 | } 86 | 87 | -(void)doActionProcessing{ 88 | WS 89 | // 用户交互处理 90 | self.usernameDidChange = Observer.create().handle(^(id _Nonnull newValue) { 91 | SS 92 | self.username.value = newValue; 93 | }); 94 | self.passwordDidChange = Observer.create().handle(^(id _Nonnull newValue) { 95 | SS 96 | self.password.value = newValue; 97 | }); 98 | self.loginTouched = Observer.create().handle(^(id _Nonnull newValue) { 99 | SS 100 | self.instruction.value = [NSString stringWithFormat:@"登录成功(*^▽^*)"]; 101 | }); 102 | } 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /MvvmDemo/MVCDemo/MVCLoginViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MVCViewController.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "MVCLoginViewController.h" 9 | #import "MVCLoginView.h" 10 | 11 | @interface MVCLoginViewController () 12 | 13 | @property(strong, nonatomic) MVCLoginView *loginView; 14 | 15 | @end 16 | 17 | @implementation MVCLoginViewController 18 | 19 | - (void)viewDidLoad { 20 | [super viewDidLoad]; 21 | 22 | self.loginView = [[MVCLoginView alloc] initWithFrame:UIScreen.mainScreen.bounds]; 23 | [self.view addSubview:self.loginView]; 24 | 25 | [self doBindings]; 26 | 27 | [self initStates]; 28 | } 29 | 30 | -(void)doBindings{ 31 | [self.loginView.loginButton addTarget:self action:@selector(login) forControlEvents:UIControlEventTouchUpInside]; 32 | [self.loginView.usernameTextField addTarget:self action:@selector(usernameChanged) forControlEvents:UIControlEventEditingChanged]; 33 | [self.loginView.passwordTextField addTarget:self action:@selector(passwordChanged) forControlEvents:UIControlEventEditingChanged]; 34 | } 35 | 36 | -(void)initStates{ 37 | self.loginView.usernameTextField.text = @"maruko"; 38 | self.loginView.passwordTextField.text = @"1234"; 39 | 40 | [self usernameChanged]; 41 | [self passwordChanged]; 42 | } 43 | 44 | //MARK: Actions 45 | -(void)login{ 46 | self.loginView.instructionLabel.text = [NSString stringWithFormat:@"登录成功(*^▽^*)"]; 47 | } 48 | 49 | -(void)usernameChanged{ 50 | NSString *usernameInstruction = [self usernameValid]; 51 | BOOL passwordValid = ![self passwordValid].length; 52 | self.loginView.usernameTextField.backgroundColor = !usernameInstruction.length ? [UIColor whiteColor] : LightRed; 53 | 54 | if(usernameInstruction.length){ 55 | self.loginView.instructionLabel.text = usernameInstruction; 56 | }else{ 57 | if(passwordValid){ 58 | self.loginView.instructionLabel.text = @""; 59 | } 60 | } 61 | 62 | [self refreshLoginButton:!usernameInstruction.length && !passwordValid]; 63 | } 64 | 65 | -(void)passwordChanged{ 66 | NSString *passwordInstruction = [self passwordValid]; 67 | self.loginView.passwordTextField.backgroundColor = !passwordInstruction.length ? [UIColor whiteColor] : LightRed; 68 | 69 | BOOL usernameValid = ![self usernameValid].length; 70 | if(usernameValid){ 71 | self.loginView.instructionLabel.text = passwordInstruction; 72 | } 73 | 74 | [self refreshLoginButton:usernameValid && !passwordInstruction.length]; 75 | } 76 | 77 | -(void)refreshLoginButton:(BOOL)loginEnabled{ 78 | self.loginView.loginButton.enabled = loginEnabled; 79 | self.loginView.loginButton.backgroundColor = loginEnabled ? ThemeColor : LightGray; 80 | [self.loginView.loginButton setTitleColor:loginEnabled ? [UIColor whiteColor] : [UIColor darkGrayColor] forState:UIControlStateNormal]; 81 | } 82 | 83 | -(NSString *)passwordValid{ 84 | NSString *password = self.loginView.passwordTextField.text; 85 | NSString *passwordInstruction; 86 | if(!password.length){ 87 | passwordInstruction = @"*密码不能为空"; 88 | }else if(password.length < 6){ 89 | passwordInstruction = @"*密码必须超过6个字符"; 90 | }else if(password.length > 24){ 91 | passwordInstruction = @"*密码不能超过24个字符"; 92 | }else{ 93 | passwordInstruction = @""; 94 | } 95 | return passwordInstruction; 96 | } 97 | 98 | -(NSString *)usernameValid{ 99 | NSString *username = self.loginView.usernameTextField.text; 100 | NSString *usernameInstruction; 101 | if(!username.length){ 102 | usernameInstruction = @"*用户名不能为空"; 103 | }else if(username.length < 6){ 104 | usernameInstruction = @"*用户名必须超过6个字符"; 105 | }else if(username.length > 24){ 106 | usernameInstruction = @"*用户名不能超过24个字符"; 107 | }else{ 108 | usernameInstruction = @""; 109 | } 110 | return usernameInstruction; 111 | } 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /MvvmDemo/DataDriven/Combine/ObservableCombiner.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableCombiner.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/3. 6 | // 7 | 8 | #import "ObservableCombiner.h" 9 | #import "Observer.h" 10 | 11 | @interface ObservableCombiner () 12 | 13 | @property(strong, nonatomic) NSMutableArray *observables; 14 | 15 | @property(assign, nonatomic) CombineStrategy strategy; 16 | 17 | @property(assign, nonatomic) NSUInteger accessFlags; 18 | 19 | @property(copy, nonatomic) ObservableCombiner * (^combine)(id observable); 20 | 21 | @property(copy, nonatomic) ObservableCombiner * (^handle)(CombinerHandler); 22 | 23 | @property(copy, nonatomic) CombinerHandler handler; 24 | 25 | @end 26 | 27 | @implementation ObservableCombiner 28 | 29 | static ObservableCombiner *(^create)(CombineStrategy strategy) = ^ObservableCombiner *(CombineStrategy strategy){ 30 | ObservableCombiner *combiner = [[ObservableCombiner alloc] init]; 31 | combiner.strategy = strategy; 32 | return combiner; 33 | }; 34 | 35 | +(ObservableCombiner *(^)(CombineStrategy))create{ 36 | return create; 37 | } 38 | 39 | -(ObservableCombiner * (^)(id))combine{ 40 | if(!_combine){ 41 | __weak typeof(self) weakSelf = self; 42 | _combine = ^ObservableCombiner * (id observable){ 43 | __strong typeof(weakSelf) strongSelf = weakSelf; 44 | 45 | NSInteger index = strongSelf.observables.count; 46 | id observer = Observer.create().handle(^(id _Nonnull newValue) { 47 | __strong typeof(weakSelf) strongSelf = weakSelf; 48 | [strongSelf handleNewValue:newValue index:index]; 49 | }); 50 | 51 | [observable addObserver:observer]; 52 | [strongSelf.observables addObject:observable]; 53 | return strongSelf; 54 | }; 55 | } 56 | return _combine; 57 | } 58 | 59 | -(ObservableCombiner * (^)(CombinerHandler))handle{ 60 | if(!_handle){ 61 | __weak typeof(self) weakSelf = self; 62 | _handle = ^ObservableCombiner *(CombinerHandler handler){ 63 | __strong typeof(weakSelf) strongSelf = weakSelf; 64 | strongSelf.handler = handler; 65 | return strongSelf; 66 | }; 67 | } 68 | return _handle; 69 | } 70 | 71 | -(NSMutableArray *)observables{ 72 | if(!_observables){ 73 | _observables = [[NSMutableArray alloc] init]; 74 | } 75 | return _observables; 76 | } 77 | 78 | -(void)handleNewValue:(id)newValue index:(NSInteger)index{ 79 | //TODO: 根据不同的策略触发完成事件 80 | BOOL isFirst = !self.accessFlags; 81 | self.accessFlags |= (1 << index); 82 | switch (self.strategy) { 83 | case CombineStrategyFirst:{ 84 | if(isFirst){ 85 | self.value = self.handler([self getAllValues]); 86 | } 87 | }break; 88 | 89 | case CombineStrategyEvery:{ 90 | self.value = self.handler([self getAllValues]); 91 | }break; 92 | 93 | case CombineStrategyAll:{ 94 | NSUInteger allFlags = pow(2, self.observables.count) - 1; 95 | BOOL isAll = (allFlags & self.accessFlags) == allFlags; 96 | if(isAll){ 97 | self.value = self.handler([self getAllValues]); 98 | } 99 | }break; 100 | 101 | default: 102 | break; 103 | } 104 | } 105 | 106 | -(NSArray *)getAllValues{ 107 | NSMutableArray *result = [[NSMutableArray alloc] init]; 108 | for(id observable in self.observables){ 109 | [result addObject:observable.value ?: [NSNull null]]; 110 | } 111 | return result; 112 | } 113 | 114 | static id _Nullable (^safeValue)(NSArray *, NSInteger) = ^id (NSArray *values, NSInteger index){ 115 | return values[index] == [NSNull null] ? nil : values[index]; 116 | }; 117 | 118 | +(id _Nullable (^)(NSArray * _Nonnull, NSInteger))safeValue{ 119 | return safeValue; 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /MvvmDemo/MVCDemo/MVCLoginView.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "MVCLoginView.h" 9 | 10 | @interface MVCLoginView () 11 | 12 | @property(strong, nonatomic) UILabel *usernameLabel; 13 | 14 | @property(strong, nonatomic) UILabel *passwordLabel; 15 | 16 | @property(strong, nonatomic) UIImageView *avatarImageView; 17 | 18 | @end 19 | 20 | @implementation MVCLoginView 21 | 22 | -(instancetype)initWithFrame:(CGRect)frame{ 23 | if(self = [super initWithFrame:frame]){ 24 | [self doLayout]; 25 | } 26 | return self; 27 | } 28 | 29 | -(void)doLayout{ 30 | self.backgroundColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 31 | [self addSubview:self.avatarImageView]; 32 | [self addSubview:self.usernameTextField]; 33 | [self addSubview:self.passwordTextField]; 34 | [self addSubview:self.loginButton]; 35 | [self addSubview:self.instructionLabel]; 36 | [self addSubview:self.usernameLabel]; 37 | [self addSubview:self.passwordLabel]; 38 | } 39 | 40 | //MARK: Getters & Setters 41 | -(UIImageView *)avatarImageView{ 42 | if(!_avatarImageView){ 43 | _avatarImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.5*UIScreen.mainScreen.bounds.size.width - 75, 90, 150, 150)]; 44 | _avatarImageView.backgroundColor = LightBlue; 45 | _avatarImageView.layer.cornerRadius = 75; 46 | _avatarImageView.layer.masksToBounds = YES; 47 | _avatarImageView.image = [UIImage imageNamed:@"avatar.jpg"]; 48 | } 49 | return _avatarImageView; 50 | } 51 | -(UITextField *)usernameTextField{ 52 | if(!_usernameTextField){ 53 | _usernameTextField = [[UITextField alloc] initWithFrame:CGRectMake(64, 280, UIScreen.mainScreen.bounds.size.width - 20 - 40 - 20, 40)]; 54 | _usernameTextField.keyboardType = UIKeyboardTypeASCIICapable; 55 | _usernameTextField.backgroundColor = [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0]; 56 | _usernameTextField.layer.cornerRadius = 20; 57 | _usernameTextField.layer.masksToBounds = YES; 58 | _usernameTextField.placeholder = @"请输入用户名"; 59 | _usernameTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 60 | _usernameTextField.leftViewMode = UITextFieldViewModeAlways; 61 | _usernameTextField.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 62 | _usernameTextField.rightViewMode = UITextFieldViewModeAlways; 63 | _usernameTextField.textColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 64 | } 65 | return _usernameTextField; 66 | } 67 | 68 | -(UITextField *)passwordTextField{ 69 | if(!_passwordTextField){ 70 | _passwordTextField = [[UITextField alloc] initWithFrame:CGRectMake(64, 340, UIScreen.mainScreen.bounds.size.width - 20 - 40 - 20, 40)]; 71 | _passwordTextField.keyboardType = UIKeyboardTypeASCIICapable; 72 | _passwordTextField.secureTextEntry = YES; 73 | _passwordTextField.backgroundColor = [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0]; 74 | _passwordTextField.layer.cornerRadius = 20; 75 | _passwordTextField.layer.masksToBounds = YES; 76 | _passwordTextField.placeholder = @"请输入密码"; 77 | _passwordTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 78 | _passwordTextField.leftViewMode = UITextFieldViewModeAlways; 79 | _passwordTextField.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 80 | _passwordTextField.rightViewMode = UITextFieldViewModeAlways; 81 | _passwordTextField.textColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 82 | } 83 | return _passwordTextField; 84 | } 85 | 86 | -(UILabel *)instructionLabel{ 87 | if(!_instructionLabel){ 88 | _instructionLabel = [[UILabel alloc] initWithFrame:CGRectMake(30, 420, UIScreen.mainScreen.bounds.size.width - 20, 24)]; 89 | _instructionLabel.textColor = [UIColor systemRedColor]; 90 | _instructionLabel.font = [UIFont systemFontOfSize:14]; 91 | } 92 | return _instructionLabel; 93 | } 94 | 95 | -(UIButton *)loginButton{ 96 | if(!_loginButton){ 97 | _loginButton = [UIButton buttonWithType:UIButtonTypeSystem]; 98 | _loginButton.frame = CGRectMake(20, 450, UIScreen.mainScreen.bounds.size.width - 40, 40); 99 | _loginButton.backgroundColor = [UIColor systemBlueColor]; 100 | [_loginButton setTitle:@"登 录" forState:UIControlStateNormal]; 101 | [_loginButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 102 | _loginButton.titleLabel.font = [UIFont systemFontOfSize:16]; 103 | _loginButton.layer.cornerRadius = 20; 104 | _loginButton.layer.masksToBounds = YES; 105 | } 106 | return _loginButton; 107 | } 108 | 109 | -(UILabel *)usernameLabel{ 110 | if(!_usernameLabel){ 111 | _usernameLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 280, 40, 40)]; 112 | _usernameLabel.text = @"用户"; 113 | _usernameLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; 114 | _usernameLabel.font = [UIFont systemFontOfSize:16]; 115 | } 116 | return _usernameLabel; 117 | } 118 | 119 | -(UILabel *)passwordLabel{ 120 | if(!_passwordLabel){ 121 | _passwordLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 340, 40, 40)]; 122 | _passwordLabel.text = @"密码"; 123 | _passwordLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; 124 | _passwordLabel.font = [UIFont systemFontOfSize:16]; 125 | } 126 | return _passwordLabel; 127 | } 128 | 129 | @end 130 | -------------------------------------------------------------------------------- /MvvmDemo/DataDrivenDemo/LoginView.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.m 3 | // MvvmDemo 4 | // 5 | // Created by Luminixus on 2021/4/4. 6 | // 7 | 8 | #import "LoginView.h" 9 | 10 | #define WS __weak typeof(self) weakSelf = self; 11 | #define SS __strong typeof(weakSelf) self = weakSelf; 12 | 13 | @interface LoginView () 14 | 15 | @property(strong, nonatomic) UILabel *usernameLabel; 16 | 17 | @property(strong, nonatomic) UILabel *passwordLabel; 18 | 19 | @property(strong, nonatomic) UIImageView *avatarImageView; 20 | 21 | @end 22 | 23 | @implementation LoginView 24 | 25 | -(instancetype)initWithFrame:(CGRect)frame{ 26 | if(self = [super initWithFrame:frame]){ 27 | [self doLayout]; 28 | 29 | [self addTargetActions]; 30 | 31 | [self makeDataDrivenRefreshing]; 32 | 33 | [self makeActionsEmittings]; 34 | } 35 | return self; 36 | } 37 | 38 | -(void)doLayout{ 39 | self.backgroundColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 40 | [self addSubview:self.avatarImageView]; 41 | [self addSubview:self.usernameTextField]; 42 | [self addSubview:self.passwordTextField]; 43 | [self addSubview:self.loginButton]; 44 | [self addSubview:self.instructionLabel]; 45 | [self addSubview:self.usernameLabel]; 46 | [self addSubview:self.passwordLabel]; 47 | } 48 | 49 | -(void)addTargetActions{ 50 | [self.loginButton addTarget:self action:@selector(login) forControlEvents:UIControlEventTouchUpInside]; 51 | [self.usernameTextField addTarget:self action:@selector(usernameChanged) forControlEvents:UIControlEventEditingChanged]; 52 | [self.passwordTextField addTarget:self action:@selector(passwordChanged) forControlEvents:UIControlEventEditingChanged]; 53 | } 54 | 55 | -(void)makeDataDrivenRefreshing{ 56 | WS 57 | self.username = Observer.create().handle(^(id _Nonnull newValue) { 58 | SS 59 | self.usernameTextField.text = newValue; 60 | }); 61 | 62 | self.password = Observer.create().handle(^(id _Nonnull newValue) { 63 | SS 64 | self.passwordTextField.text = newValue; 65 | }); 66 | 67 | self.instruction = Observer.create().handle(^(id _Nonnull newValue) { 68 | SS 69 | self.instructionLabel.text = newValue; 70 | }); 71 | 72 | self.usernameValid = Observer.create().handle(^(id _Nonnull newValue) { 73 | SS 74 | self.usernameTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed; 75 | }); 76 | 77 | self.passwordValid = Observer.create().handle(^(id _Nonnull newValue) { 78 | SS 79 | self.passwordTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed; 80 | }); 81 | 82 | self.loginEnabled = Observer.create().handle(^(id _Nonnull newValue) { 83 | SS 84 | self.loginButton.enabled = [newValue boolValue]; 85 | self.loginButton.backgroundColor = [newValue boolValue] ? ThemeColor : LightGray; 86 | [self.loginButton setTitleColor:[newValue boolValue] ? [UIColor whiteColor] : [UIColor darkGrayColor] forState:UIControlStateNormal]; 87 | }); 88 | } 89 | 90 | -(void)makeActionsEmittings{ 91 | self.usernameDidChange = Observable.create(nil); 92 | self.passwordDidChange = Observable.create(nil); 93 | self.loginButtonTouched = Observable.create(nil); 94 | } 95 | 96 | //MARK: Actions 97 | -(void)login{ 98 | self.loginButtonTouched.value = nil; 99 | } 100 | 101 | -(void)usernameChanged{ 102 | self.usernameDidChange.value = self.usernameTextField.text; 103 | } 104 | 105 | -(void)passwordChanged{ 106 | self.passwordDidChange.value = self.passwordTextField.text; 107 | } 108 | 109 | //MARK: Getters & Setters 110 | -(UIImageView *)avatarImageView{ 111 | if(!_avatarImageView){ 112 | _avatarImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.5*UIScreen.mainScreen.bounds.size.width - 75, 90, 150, 150)]; 113 | _avatarImageView.backgroundColor = LightBlue; 114 | _avatarImageView.layer.cornerRadius = 75; 115 | _avatarImageView.layer.masksToBounds = YES; 116 | _avatarImageView.image = [UIImage imageNamed:@"avatar.jpg"]; 117 | } 118 | return _avatarImageView; 119 | } 120 | -(UITextField *)usernameTextField{ 121 | if(!_usernameTextField){ 122 | _usernameTextField = [[UITextField alloc] initWithFrame:CGRectMake(64, 280, UIScreen.mainScreen.bounds.size.width - 20 - 40 - 20, 40)]; 123 | _usernameTextField.keyboardType = UIKeyboardTypeASCIICapable; 124 | _usernameTextField.backgroundColor = [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0]; 125 | _usernameTextField.layer.cornerRadius = 20; 126 | _usernameTextField.layer.masksToBounds = YES; 127 | _usernameTextField.placeholder = @"请输入用户名"; 128 | _usernameTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 129 | _usernameTextField.leftViewMode = UITextFieldViewModeAlways; 130 | _usernameTextField.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 131 | _usernameTextField.rightViewMode = UITextFieldViewModeAlways; 132 | _usernameTextField.textColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 133 | } 134 | return _usernameTextField; 135 | } 136 | 137 | -(UITextField *)passwordTextField{ 138 | if(!_passwordTextField){ 139 | _passwordTextField = [[UITextField alloc] initWithFrame:CGRectMake(64, 340, UIScreen.mainScreen.bounds.size.width - 20 - 40 - 20, 40)]; 140 | _passwordTextField.keyboardType = UIKeyboardTypeASCIICapable; 141 | _passwordTextField.secureTextEntry = YES; 142 | _passwordTextField.backgroundColor = [UIColor colorWithRed:1.0*0xCC/0xFF green:1.0*0xCC/0xFF blue:1.0*0xFF/0xFF alpha:1.0]; 143 | _passwordTextField.layer.cornerRadius = 20; 144 | _passwordTextField.layer.masksToBounds = YES; 145 | _passwordTextField.placeholder = @"请输入密码"; 146 | _passwordTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 147 | _passwordTextField.leftViewMode = UITextFieldViewModeAlways; 148 | _passwordTextField.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 16, 10)]; 149 | _passwordTextField.rightViewMode = UITextFieldViewModeAlways; 150 | _passwordTextField.textColor = [UIColor colorWithWhite:1.0*0x1A/0xFF alpha:1.0]; 151 | } 152 | return _passwordTextField; 153 | } 154 | 155 | -(UILabel *)instructionLabel{ 156 | if(!_instructionLabel){ 157 | _instructionLabel = [[UILabel alloc] initWithFrame:CGRectMake(30, 420, UIScreen.mainScreen.bounds.size.width - 20, 24)]; 158 | _instructionLabel.textColor = [UIColor systemRedColor]; 159 | _instructionLabel.font = [UIFont systemFontOfSize:14]; 160 | } 161 | return _instructionLabel; 162 | } 163 | 164 | -(UIButton *)loginButton{ 165 | if(!_loginButton){ 166 | _loginButton = [UIButton buttonWithType:UIButtonTypeSystem]; 167 | _loginButton.frame = CGRectMake(20, 450, UIScreen.mainScreen.bounds.size.width - 40, 40); 168 | _loginButton.backgroundColor = [UIColor systemBlueColor]; 169 | [_loginButton setTitle:@"登 录" forState:UIControlStateNormal]; 170 | [_loginButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 171 | _loginButton.titleLabel.font = [UIFont systemFontOfSize:16]; 172 | _loginButton.layer.cornerRadius = 20; 173 | _loginButton.layer.masksToBounds = YES; 174 | } 175 | return _loginButton; 176 | } 177 | 178 | -(UILabel *)usernameLabel{ 179 | if(!_usernameLabel){ 180 | _usernameLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 280, 40, 40)]; 181 | _usernameLabel.text = @"用户"; 182 | _usernameLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; 183 | _usernameLabel.font = [UIFont systemFontOfSize:16]; 184 | } 185 | return _usernameLabel; 186 | } 187 | 188 | -(UILabel *)passwordLabel{ 189 | if(!_passwordLabel){ 190 | _passwordLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 340, 40, 40)]; 191 | _passwordLabel.text = @"密码"; 192 | _passwordLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; 193 | _passwordLabel.font = [UIFont systemFontOfSize:16]; 194 | } 195 | return _passwordLabel; 196 | } 197 | 198 | @end 199 | -------------------------------------------------------------------------------- /MvvmDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1F3E73DB26180618002D304C /* Observable.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F3E73DA26180618002D304C /* Observable.m */; }; 11 | 1F3E73E126180625002D304C /* Observer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F3E73E026180625002D304C /* Observer.m */; }; 12 | 1F3E73EF26181EB5002D304C /* ObservableCombiner.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F3E73EE26181EB5002D304C /* ObservableCombiner.m */; }; 13 | 1F6D415B25FDFC9400DF94F5 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D415A25FDFC9400DF94F5 /* AppDelegate.m */; }; 14 | 1F6D415E25FDFC9400DF94F5 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D415D25FDFC9400DF94F5 /* SceneDelegate.m */; }; 15 | 1F6D416125FDFC9400DF94F5 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D416025FDFC9400DF94F5 /* ViewController.m */; }; 16 | 1F6D416425FDFC9400DF94F5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1F6D416225FDFC9400DF94F5 /* Main.storyboard */; }; 17 | 1F6D416625FDFC9600DF94F5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F6D416525FDFC9600DF94F5 /* Assets.xcassets */; }; 18 | 1F6D416925FDFC9600DF94F5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1F6D416725FDFC9600DF94F5 /* LaunchScreen.storyboard */; }; 19 | 1F6D416C25FDFC9600DF94F5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D416B25FDFC9600DF94F5 /* main.m */; }; 20 | 1F6D418125FDFC9600DF94F5 /* MvvmDemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F6D418025FDFC9600DF94F5 /* MvvmDemoUITests.m */; }; 21 | 4AB106BD2653AD34000CD066 /* LoginViewModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AB106BC2653AD34000CD066 /* LoginViewModelTests.m */; }; 22 | 4AB106D12653B347000CD066 /* LoginViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AB106D02653B347000CD066 /* LoginViewTests.m */; }; 23 | 4ABF0F712618D1AD00FDF70C /* LoginViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF0F702618D1AD00FDF70C /* LoginViewModel.m */; }; 24 | 4ABF0F7B2618D2F200FDF70C /* LoginView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF0F7A2618D2F200FDF70C /* LoginView.m */; }; 25 | 4ABF0F842618D53800FDF70C /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF0F832618D53800FDF70C /* LoginViewController.m */; }; 26 | 4ABF0F902619AF6800FDF70C /* avatar.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4ABF0F8F2619AF6800FDF70C /* avatar.jpg */; }; 27 | 4ABF0F9D2619D15C00FDF70C /* MVCLoginView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF0F9C2619D15C00FDF70C /* MVCLoginView.m */; }; 28 | 4ABF0FA62619D6F900FDF70C /* MVCLoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF0FA52619D6F900FDF70C /* MVCLoginViewController.m */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | 1F6D417225FDFC9600DF94F5 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 1F6D414E25FDFC9400DF94F5 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 1F6D415525FDFC9400DF94F5; 37 | remoteInfo = MvvmDemo; 38 | }; 39 | 1F6D417D25FDFC9600DF94F5 /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = 1F6D414E25FDFC9400DF94F5 /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = 1F6D415525FDFC9400DF94F5; 44 | remoteInfo = MvvmDemo; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 1F3E73D926180618002D304C /* Observable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Observable.h; sourceTree = ""; }; 50 | 1F3E73DA26180618002D304C /* Observable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Observable.m; sourceTree = ""; }; 51 | 1F3E73DF26180625002D304C /* Observer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Observer.h; sourceTree = ""; }; 52 | 1F3E73E026180625002D304C /* Observer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Observer.m; sourceTree = ""; }; 53 | 1F3E73ED26181EB5002D304C /* ObservableCombiner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObservableCombiner.h; sourceTree = ""; }; 54 | 1F3E73EE26181EB5002D304C /* ObservableCombiner.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObservableCombiner.m; sourceTree = ""; }; 55 | 1F6D415625FDFC9400DF94F5 /* MvvmDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MvvmDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 1F6D415925FDFC9400DF94F5 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 57 | 1F6D415A25FDFC9400DF94F5 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 58 | 1F6D415C25FDFC9400DF94F5 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 59 | 1F6D415D25FDFC9400DF94F5 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 60 | 1F6D415F25FDFC9400DF94F5 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 61 | 1F6D416025FDFC9400DF94F5 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 62 | 1F6D416325FDFC9400DF94F5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63 | 1F6D416525FDFC9600DF94F5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 64 | 1F6D416825FDFC9600DF94F5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 65 | 1F6D416A25FDFC9600DF94F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 1F6D416B25FDFC9600DF94F5 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 67 | 1F6D417125FDFC9600DF94F5 /* MvvmDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MvvmDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 1F6D417725FDFC9600DF94F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 1F6D417C25FDFC9600DF94F5 /* MvvmDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MvvmDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 1F6D418025FDFC9600DF94F5 /* MvvmDemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MvvmDemoUITests.m; sourceTree = ""; }; 71 | 1F6D418225FDFC9600DF94F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 72 | 4AB106BC2653AD34000CD066 /* LoginViewModelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewModelTests.m; sourceTree = ""; }; 73 | 4AB106D02653B347000CD066 /* LoginViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewTests.m; sourceTree = ""; }; 74 | 4ABF0F6F2618D1AD00FDF70C /* LoginViewModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginViewModel.h; sourceTree = ""; }; 75 | 4ABF0F702618D1AD00FDF70C /* LoginViewModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewModel.m; sourceTree = ""; }; 76 | 4ABF0F792618D2F200FDF70C /* LoginView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginView.h; sourceTree = ""; }; 77 | 4ABF0F7A2618D2F200FDF70C /* LoginView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginView.m; sourceTree = ""; }; 78 | 4ABF0F822618D53800FDF70C /* LoginViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = ""; }; 79 | 4ABF0F832618D53800FDF70C /* LoginViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = ""; }; 80 | 4ABF0F8F2619AF6800FDF70C /* avatar.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = avatar.jpg; sourceTree = ""; }; 81 | 4ABF0F9B2619D15C00FDF70C /* MVCLoginView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVCLoginView.h; sourceTree = ""; }; 82 | 4ABF0F9C2619D15C00FDF70C /* MVCLoginView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MVCLoginView.m; sourceTree = ""; }; 83 | 4ABF0FA42619D6F900FDF70C /* MVCLoginViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVCLoginViewController.h; sourceTree = ""; }; 84 | 4ABF0FA52619D6F900FDF70C /* MVCLoginViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MVCLoginViewController.m; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | 1F6D415325FDFC9400DF94F5 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | 1F6D416E25FDFC9600DF94F5 /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | 1F6D417925FDFC9600DF94F5 /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXFrameworksBuildPhase section */ 110 | 111 | /* Begin PBXGroup section */ 112 | 1F3E73D8261805FF002D304C /* DataDriven */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 1F3E73EB26181E7B002D304C /* Base */, 116 | 1F3E73EC26181E8C002D304C /* Combine */, 117 | ); 118 | path = DataDriven; 119 | sourceTree = ""; 120 | }; 121 | 1F3E73EB26181E7B002D304C /* Base */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 1F3E73D926180618002D304C /* Observable.h */, 125 | 1F3E73DA26180618002D304C /* Observable.m */, 126 | 1F3E73DF26180625002D304C /* Observer.h */, 127 | 1F3E73E026180625002D304C /* Observer.m */, 128 | ); 129 | path = Base; 130 | sourceTree = ""; 131 | }; 132 | 1F3E73EC26181E8C002D304C /* Combine */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 1F3E73ED26181EB5002D304C /* ObservableCombiner.h */, 136 | 1F3E73EE26181EB5002D304C /* ObservableCombiner.m */, 137 | ); 138 | path = Combine; 139 | sourceTree = ""; 140 | }; 141 | 1F6D414D25FDFC9400DF94F5 = { 142 | isa = PBXGroup; 143 | children = ( 144 | 1F6D415825FDFC9400DF94F5 /* MvvmDemo */, 145 | 1F6D417425FDFC9600DF94F5 /* MvvmDemoTests */, 146 | 1F6D417F25FDFC9600DF94F5 /* MvvmDemoUITests */, 147 | 1F6D415725FDFC9400DF94F5 /* Products */, 148 | ); 149 | sourceTree = ""; 150 | }; 151 | 1F6D415725FDFC9400DF94F5 /* Products */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 1F6D415625FDFC9400DF94F5 /* MvvmDemo.app */, 155 | 1F6D417125FDFC9600DF94F5 /* MvvmDemoTests.xctest */, 156 | 1F6D417C25FDFC9600DF94F5 /* MvvmDemoUITests.xctest */, 157 | ); 158 | name = Products; 159 | sourceTree = ""; 160 | }; 161 | 1F6D415825FDFC9400DF94F5 /* MvvmDemo */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 4ABF0F9A2619D11700FDF70C /* MVCDemo */, 165 | 4ABF0F6E2618D18000FDF70C /* DataDrivenDemo */, 166 | 1F3E73D8261805FF002D304C /* DataDriven */, 167 | 1F6D415925FDFC9400DF94F5 /* AppDelegate.h */, 168 | 1F6D415A25FDFC9400DF94F5 /* AppDelegate.m */, 169 | 1F6D415C25FDFC9400DF94F5 /* SceneDelegate.h */, 170 | 1F6D415D25FDFC9400DF94F5 /* SceneDelegate.m */, 171 | 1F6D415F25FDFC9400DF94F5 /* ViewController.h */, 172 | 1F6D416025FDFC9400DF94F5 /* ViewController.m */, 173 | 1F6D416225FDFC9400DF94F5 /* Main.storyboard */, 174 | 1F6D416525FDFC9600DF94F5 /* Assets.xcassets */, 175 | 1F6D416725FDFC9600DF94F5 /* LaunchScreen.storyboard */, 176 | 1F6D416A25FDFC9600DF94F5 /* Info.plist */, 177 | 1F6D416B25FDFC9600DF94F5 /* main.m */, 178 | ); 179 | path = MvvmDemo; 180 | sourceTree = ""; 181 | }; 182 | 1F6D417425FDFC9600DF94F5 /* MvvmDemoTests */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 4AB106B82653ACF1000CD066 /* DataDrivenDemoTests */, 186 | 1F6D417725FDFC9600DF94F5 /* Info.plist */, 187 | ); 188 | path = MvvmDemoTests; 189 | sourceTree = ""; 190 | }; 191 | 1F6D417F25FDFC9600DF94F5 /* MvvmDemoUITests */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 1F6D418025FDFC9600DF94F5 /* MvvmDemoUITests.m */, 195 | 1F6D418225FDFC9600DF94F5 /* Info.plist */, 196 | ); 197 | path = MvvmDemoUITests; 198 | sourceTree = ""; 199 | }; 200 | 4AB106B82653ACF1000CD066 /* DataDrivenDemoTests */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | 4AB106BC2653AD34000CD066 /* LoginViewModelTests.m */, 204 | 4AB106D02653B347000CD066 /* LoginViewTests.m */, 205 | ); 206 | path = DataDrivenDemoTests; 207 | sourceTree = ""; 208 | }; 209 | 4ABF0F6E2618D18000FDF70C /* DataDrivenDemo */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 4ABF0F8E2619AF5600FDF70C /* Resources */, 213 | 4ABF0F6F2618D1AD00FDF70C /* LoginViewModel.h */, 214 | 4ABF0F702618D1AD00FDF70C /* LoginViewModel.m */, 215 | 4ABF0F792618D2F200FDF70C /* LoginView.h */, 216 | 4ABF0F7A2618D2F200FDF70C /* LoginView.m */, 217 | 4ABF0F822618D53800FDF70C /* LoginViewController.h */, 218 | 4ABF0F832618D53800FDF70C /* LoginViewController.m */, 219 | ); 220 | path = DataDrivenDemo; 221 | sourceTree = ""; 222 | }; 223 | 4ABF0F8E2619AF5600FDF70C /* Resources */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 4ABF0F8F2619AF6800FDF70C /* avatar.jpg */, 227 | ); 228 | path = Resources; 229 | sourceTree = ""; 230 | }; 231 | 4ABF0F9A2619D11700FDF70C /* MVCDemo */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 4ABF0F9B2619D15C00FDF70C /* MVCLoginView.h */, 235 | 4ABF0F9C2619D15C00FDF70C /* MVCLoginView.m */, 236 | 4ABF0FA42619D6F900FDF70C /* MVCLoginViewController.h */, 237 | 4ABF0FA52619D6F900FDF70C /* MVCLoginViewController.m */, 238 | ); 239 | path = MVCDemo; 240 | sourceTree = ""; 241 | }; 242 | /* End PBXGroup section */ 243 | 244 | /* Begin PBXNativeTarget section */ 245 | 1F6D415525FDFC9400DF94F5 /* MvvmDemo */ = { 246 | isa = PBXNativeTarget; 247 | buildConfigurationList = 1F6D418525FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemo" */; 248 | buildPhases = ( 249 | 1F6D415225FDFC9400DF94F5 /* Sources */, 250 | 1F6D415325FDFC9400DF94F5 /* Frameworks */, 251 | 1F6D415425FDFC9400DF94F5 /* Resources */, 252 | ); 253 | buildRules = ( 254 | ); 255 | dependencies = ( 256 | ); 257 | name = MvvmDemo; 258 | productName = MvvmDemo; 259 | productReference = 1F6D415625FDFC9400DF94F5 /* MvvmDemo.app */; 260 | productType = "com.apple.product-type.application"; 261 | }; 262 | 1F6D417025FDFC9600DF94F5 /* MvvmDemoTests */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 1F6D418825FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemoTests" */; 265 | buildPhases = ( 266 | 1F6D416D25FDFC9600DF94F5 /* Sources */, 267 | 1F6D416E25FDFC9600DF94F5 /* Frameworks */, 268 | 1F6D416F25FDFC9600DF94F5 /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | 1F6D417325FDFC9600DF94F5 /* PBXTargetDependency */, 274 | ); 275 | name = MvvmDemoTests; 276 | productName = MvvmDemoTests; 277 | productReference = 1F6D417125FDFC9600DF94F5 /* MvvmDemoTests.xctest */; 278 | productType = "com.apple.product-type.bundle.unit-test"; 279 | }; 280 | 1F6D417B25FDFC9600DF94F5 /* MvvmDemoUITests */ = { 281 | isa = PBXNativeTarget; 282 | buildConfigurationList = 1F6D418B25FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemoUITests" */; 283 | buildPhases = ( 284 | 1F6D417825FDFC9600DF94F5 /* Sources */, 285 | 1F6D417925FDFC9600DF94F5 /* Frameworks */, 286 | 1F6D417A25FDFC9600DF94F5 /* Resources */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | 1F6D417E25FDFC9600DF94F5 /* PBXTargetDependency */, 292 | ); 293 | name = MvvmDemoUITests; 294 | productName = MvvmDemoUITests; 295 | productReference = 1F6D417C25FDFC9600DF94F5 /* MvvmDemoUITests.xctest */; 296 | productType = "com.apple.product-type.bundle.ui-testing"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | 1F6D414E25FDFC9400DF94F5 /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | LastUpgradeCheck = 1230; 305 | TargetAttributes = { 306 | 1F6D415525FDFC9400DF94F5 = { 307 | CreatedOnToolsVersion = 12.3; 308 | }; 309 | 1F6D417025FDFC9600DF94F5 = { 310 | CreatedOnToolsVersion = 12.3; 311 | TestTargetID = 1F6D415525FDFC9400DF94F5; 312 | }; 313 | 1F6D417B25FDFC9600DF94F5 = { 314 | CreatedOnToolsVersion = 12.3; 315 | TestTargetID = 1F6D415525FDFC9400DF94F5; 316 | }; 317 | }; 318 | }; 319 | buildConfigurationList = 1F6D415125FDFC9400DF94F5 /* Build configuration list for PBXProject "MvvmDemo" */; 320 | compatibilityVersion = "Xcode 9.3"; 321 | developmentRegion = en; 322 | hasScannedForEncodings = 0; 323 | knownRegions = ( 324 | en, 325 | Base, 326 | ); 327 | mainGroup = 1F6D414D25FDFC9400DF94F5; 328 | productRefGroup = 1F6D415725FDFC9400DF94F5 /* Products */; 329 | projectDirPath = ""; 330 | projectRoot = ""; 331 | targets = ( 332 | 1F6D415525FDFC9400DF94F5 /* MvvmDemo */, 333 | 1F6D417025FDFC9600DF94F5 /* MvvmDemoTests */, 334 | 1F6D417B25FDFC9600DF94F5 /* MvvmDemoUITests */, 335 | ); 336 | }; 337 | /* End PBXProject section */ 338 | 339 | /* Begin PBXResourcesBuildPhase section */ 340 | 1F6D415425FDFC9400DF94F5 /* Resources */ = { 341 | isa = PBXResourcesBuildPhase; 342 | buildActionMask = 2147483647; 343 | files = ( 344 | 4ABF0F902619AF6800FDF70C /* avatar.jpg in Resources */, 345 | 1F6D416925FDFC9600DF94F5 /* LaunchScreen.storyboard in Resources */, 346 | 1F6D416625FDFC9600DF94F5 /* Assets.xcassets in Resources */, 347 | 1F6D416425FDFC9400DF94F5 /* Main.storyboard in Resources */, 348 | ); 349 | runOnlyForDeploymentPostprocessing = 0; 350 | }; 351 | 1F6D416F25FDFC9600DF94F5 /* Resources */ = { 352 | isa = PBXResourcesBuildPhase; 353 | buildActionMask = 2147483647; 354 | files = ( 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | }; 358 | 1F6D417A25FDFC9600DF94F5 /* Resources */ = { 359 | isa = PBXResourcesBuildPhase; 360 | buildActionMask = 2147483647; 361 | files = ( 362 | ); 363 | runOnlyForDeploymentPostprocessing = 0; 364 | }; 365 | /* End PBXResourcesBuildPhase section */ 366 | 367 | /* Begin PBXSourcesBuildPhase section */ 368 | 1F6D415225FDFC9400DF94F5 /* Sources */ = { 369 | isa = PBXSourcesBuildPhase; 370 | buildActionMask = 2147483647; 371 | files = ( 372 | 1F3E73DB26180618002D304C /* Observable.m in Sources */, 373 | 1F6D416125FDFC9400DF94F5 /* ViewController.m in Sources */, 374 | 1F6D415B25FDFC9400DF94F5 /* AppDelegate.m in Sources */, 375 | 4ABF0FA62619D6F900FDF70C /* MVCLoginViewController.m in Sources */, 376 | 4ABF0F9D2619D15C00FDF70C /* MVCLoginView.m in Sources */, 377 | 1F3E73EF26181EB5002D304C /* ObservableCombiner.m in Sources */, 378 | 1F3E73E126180625002D304C /* Observer.m in Sources */, 379 | 4ABF0F712618D1AD00FDF70C /* LoginViewModel.m in Sources */, 380 | 1F6D416C25FDFC9600DF94F5 /* main.m in Sources */, 381 | 4ABF0F7B2618D2F200FDF70C /* LoginView.m in Sources */, 382 | 4ABF0F842618D53800FDF70C /* LoginViewController.m in Sources */, 383 | 1F6D415E25FDFC9400DF94F5 /* SceneDelegate.m in Sources */, 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | 1F6D416D25FDFC9600DF94F5 /* Sources */ = { 388 | isa = PBXSourcesBuildPhase; 389 | buildActionMask = 2147483647; 390 | files = ( 391 | 4AB106D12653B347000CD066 /* LoginViewTests.m in Sources */, 392 | 4AB106BD2653AD34000CD066 /* LoginViewModelTests.m in Sources */, 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | 1F6D417825FDFC9600DF94F5 /* Sources */ = { 397 | isa = PBXSourcesBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | 1F6D418125FDFC9600DF94F5 /* MvvmDemoUITests.m in Sources */, 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | /* End PBXSourcesBuildPhase section */ 405 | 406 | /* Begin PBXTargetDependency section */ 407 | 1F6D417325FDFC9600DF94F5 /* PBXTargetDependency */ = { 408 | isa = PBXTargetDependency; 409 | target = 1F6D415525FDFC9400DF94F5 /* MvvmDemo */; 410 | targetProxy = 1F6D417225FDFC9600DF94F5 /* PBXContainerItemProxy */; 411 | }; 412 | 1F6D417E25FDFC9600DF94F5 /* PBXTargetDependency */ = { 413 | isa = PBXTargetDependency; 414 | target = 1F6D415525FDFC9400DF94F5 /* MvvmDemo */; 415 | targetProxy = 1F6D417D25FDFC9600DF94F5 /* PBXContainerItemProxy */; 416 | }; 417 | /* End PBXTargetDependency section */ 418 | 419 | /* Begin PBXVariantGroup section */ 420 | 1F6D416225FDFC9400DF94F5 /* Main.storyboard */ = { 421 | isa = PBXVariantGroup; 422 | children = ( 423 | 1F6D416325FDFC9400DF94F5 /* Base */, 424 | ); 425 | name = Main.storyboard; 426 | sourceTree = ""; 427 | }; 428 | 1F6D416725FDFC9600DF94F5 /* LaunchScreen.storyboard */ = { 429 | isa = PBXVariantGroup; 430 | children = ( 431 | 1F6D416825FDFC9600DF94F5 /* Base */, 432 | ); 433 | name = LaunchScreen.storyboard; 434 | sourceTree = ""; 435 | }; 436 | /* End PBXVariantGroup section */ 437 | 438 | /* Begin XCBuildConfiguration section */ 439 | 1F6D418325FDFC9600DF94F5 /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ALWAYS_SEARCH_USER_PATHS = NO; 443 | CLANG_ANALYZER_NONNULL = YES; 444 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 445 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 446 | CLANG_CXX_LIBRARY = "libc++"; 447 | CLANG_ENABLE_MODULES = YES; 448 | CLANG_ENABLE_OBJC_ARC = YES; 449 | CLANG_ENABLE_OBJC_WEAK = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 455 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 456 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 457 | CLANG_WARN_EMPTY_BODY = YES; 458 | CLANG_WARN_ENUM_CONVERSION = YES; 459 | CLANG_WARN_INFINITE_RECURSION = YES; 460 | CLANG_WARN_INT_CONVERSION = YES; 461 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 463 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 464 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 465 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 466 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 467 | CLANG_WARN_STRICT_PROTOTYPES = YES; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 470 | CLANG_WARN_UNREACHABLE_CODE = YES; 471 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 472 | COPY_PHASE_STRIP = NO; 473 | DEBUG_INFORMATION_FORMAT = dwarf; 474 | ENABLE_STRICT_OBJC_MSGSEND = YES; 475 | ENABLE_TESTABILITY = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_DYNAMIC_NO_PIC = NO; 478 | GCC_NO_COMMON_BLOCKS = YES; 479 | GCC_OPTIMIZATION_LEVEL = 0; 480 | GCC_PREPROCESSOR_DEFINITIONS = ( 481 | "DEBUG=1", 482 | "$(inherited)", 483 | ); 484 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 485 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 486 | GCC_WARN_UNDECLARED_SELECTOR = YES; 487 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 488 | GCC_WARN_UNUSED_FUNCTION = YES; 489 | GCC_WARN_UNUSED_VARIABLE = YES; 490 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 491 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 492 | MTL_FAST_MATH = YES; 493 | ONLY_ACTIVE_ARCH = YES; 494 | SDKROOT = iphoneos; 495 | }; 496 | name = Debug; 497 | }; 498 | 1F6D418425FDFC9600DF94F5 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | ALWAYS_SEARCH_USER_PATHS = NO; 502 | CLANG_ANALYZER_NONNULL = YES; 503 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 504 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 505 | CLANG_CXX_LIBRARY = "libc++"; 506 | CLANG_ENABLE_MODULES = YES; 507 | CLANG_ENABLE_OBJC_ARC = YES; 508 | CLANG_ENABLE_OBJC_WEAK = YES; 509 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 510 | CLANG_WARN_BOOL_CONVERSION = YES; 511 | CLANG_WARN_COMMA = YES; 512 | CLANG_WARN_CONSTANT_CONVERSION = YES; 513 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 514 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 515 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 516 | CLANG_WARN_EMPTY_BODY = YES; 517 | CLANG_WARN_ENUM_CONVERSION = YES; 518 | CLANG_WARN_INFINITE_RECURSION = YES; 519 | CLANG_WARN_INT_CONVERSION = YES; 520 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 521 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 522 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 523 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 524 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 525 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 526 | CLANG_WARN_STRICT_PROTOTYPES = YES; 527 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 528 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 529 | CLANG_WARN_UNREACHABLE_CODE = YES; 530 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 531 | COPY_PHASE_STRIP = NO; 532 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 533 | ENABLE_NS_ASSERTIONS = NO; 534 | ENABLE_STRICT_OBJC_MSGSEND = YES; 535 | GCC_C_LANGUAGE_STANDARD = gnu11; 536 | GCC_NO_COMMON_BLOCKS = YES; 537 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 538 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 539 | GCC_WARN_UNDECLARED_SELECTOR = YES; 540 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 541 | GCC_WARN_UNUSED_FUNCTION = YES; 542 | GCC_WARN_UNUSED_VARIABLE = YES; 543 | IPHONEOS_DEPLOYMENT_TARGET = 14.3; 544 | MTL_ENABLE_DEBUG_INFO = NO; 545 | MTL_FAST_MATH = YES; 546 | SDKROOT = iphoneos; 547 | VALIDATE_PRODUCT = YES; 548 | }; 549 | name = Release; 550 | }; 551 | 1F6D418625FDFC9600DF94F5 /* Debug */ = { 552 | isa = XCBuildConfiguration; 553 | buildSettings = { 554 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 555 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 556 | CODE_SIGN_STYLE = Automatic; 557 | INFOPLIST_FILE = MvvmDemo/Info.plist; 558 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 559 | LD_RUNPATH_SEARCH_PATHS = ( 560 | "$(inherited)", 561 | "@executable_path/Frameworks", 562 | ); 563 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemo; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | TARGETED_DEVICE_FAMILY = "1,2"; 566 | }; 567 | name = Debug; 568 | }; 569 | 1F6D418725FDFC9600DF94F5 /* Release */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 573 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 574 | CODE_SIGN_STYLE = Automatic; 575 | INFOPLIST_FILE = MvvmDemo/Info.plist; 576 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 577 | LD_RUNPATH_SEARCH_PATHS = ( 578 | "$(inherited)", 579 | "@executable_path/Frameworks", 580 | ); 581 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemo; 582 | PRODUCT_NAME = "$(TARGET_NAME)"; 583 | TARGETED_DEVICE_FAMILY = "1,2"; 584 | }; 585 | name = Release; 586 | }; 587 | 1F6D418925FDFC9600DF94F5 /* Debug */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | BUNDLE_LOADER = "$(TEST_HOST)"; 591 | CODE_SIGN_STYLE = Automatic; 592 | HEADER_SEARCH_PATHS = ( 593 | "$(SRCROOT)/MvvmDemo/DataDrivenDemo/**", 594 | "$(SRCROOT)/MvvmDemo/DataDriven/**", 595 | ); 596 | INFOPLIST_FILE = MvvmDemoTests/Info.plist; 597 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 598 | LD_RUNPATH_SEARCH_PATHS = ( 599 | "$(inherited)", 600 | "@executable_path/Frameworks", 601 | "@loader_path/Frameworks", 602 | ); 603 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemoTests; 604 | PRODUCT_NAME = "$(TARGET_NAME)"; 605 | TARGETED_DEVICE_FAMILY = "1,2"; 606 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MvvmDemo.app/MvvmDemo"; 607 | }; 608 | name = Debug; 609 | }; 610 | 1F6D418A25FDFC9600DF94F5 /* Release */ = { 611 | isa = XCBuildConfiguration; 612 | buildSettings = { 613 | BUNDLE_LOADER = "$(TEST_HOST)"; 614 | CODE_SIGN_STYLE = Automatic; 615 | HEADER_SEARCH_PATHS = ( 616 | "$(SRCROOT)/MvvmDemo/DataDrivenDemo/**", 617 | "$(SRCROOT)/MvvmDemo/DataDriven/**", 618 | ); 619 | INFOPLIST_FILE = MvvmDemoTests/Info.plist; 620 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 621 | LD_RUNPATH_SEARCH_PATHS = ( 622 | "$(inherited)", 623 | "@executable_path/Frameworks", 624 | "@loader_path/Frameworks", 625 | ); 626 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemoTests; 627 | PRODUCT_NAME = "$(TARGET_NAME)"; 628 | TARGETED_DEVICE_FAMILY = "1,2"; 629 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MvvmDemo.app/MvvmDemo"; 630 | }; 631 | name = Release; 632 | }; 633 | 1F6D418C25FDFC9600DF94F5 /* Debug */ = { 634 | isa = XCBuildConfiguration; 635 | buildSettings = { 636 | CODE_SIGN_STYLE = Automatic; 637 | INFOPLIST_FILE = MvvmDemoUITests/Info.plist; 638 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 639 | LD_RUNPATH_SEARCH_PATHS = ( 640 | "$(inherited)", 641 | "@executable_path/Frameworks", 642 | "@loader_path/Frameworks", 643 | ); 644 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemoUITests; 645 | PRODUCT_NAME = "$(TARGET_NAME)"; 646 | TARGETED_DEVICE_FAMILY = "1,2"; 647 | TEST_TARGET_NAME = MvvmDemo; 648 | }; 649 | name = Debug; 650 | }; 651 | 1F6D418D25FDFC9600DF94F5 /* Release */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | CODE_SIGN_STYLE = Automatic; 655 | INFOPLIST_FILE = MvvmDemoUITests/Info.plist; 656 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 657 | LD_RUNPATH_SEARCH_PATHS = ( 658 | "$(inherited)", 659 | "@executable_path/Frameworks", 660 | "@loader_path/Frameworks", 661 | ); 662 | PRODUCT_BUNDLE_IDENTIFIER = cn.mastercom.MvvmDemoUITests; 663 | PRODUCT_NAME = "$(TARGET_NAME)"; 664 | TARGETED_DEVICE_FAMILY = "1,2"; 665 | TEST_TARGET_NAME = MvvmDemo; 666 | }; 667 | name = Release; 668 | }; 669 | /* End XCBuildConfiguration section */ 670 | 671 | /* Begin XCConfigurationList section */ 672 | 1F6D415125FDFC9400DF94F5 /* Build configuration list for PBXProject "MvvmDemo" */ = { 673 | isa = XCConfigurationList; 674 | buildConfigurations = ( 675 | 1F6D418325FDFC9600DF94F5 /* Debug */, 676 | 1F6D418425FDFC9600DF94F5 /* Release */, 677 | ); 678 | defaultConfigurationIsVisible = 0; 679 | defaultConfigurationName = Release; 680 | }; 681 | 1F6D418525FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemo" */ = { 682 | isa = XCConfigurationList; 683 | buildConfigurations = ( 684 | 1F6D418625FDFC9600DF94F5 /* Debug */, 685 | 1F6D418725FDFC9600DF94F5 /* Release */, 686 | ); 687 | defaultConfigurationIsVisible = 0; 688 | defaultConfigurationName = Release; 689 | }; 690 | 1F6D418825FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemoTests" */ = { 691 | isa = XCConfigurationList; 692 | buildConfigurations = ( 693 | 1F6D418925FDFC9600DF94F5 /* Debug */, 694 | 1F6D418A25FDFC9600DF94F5 /* Release */, 695 | ); 696 | defaultConfigurationIsVisible = 0; 697 | defaultConfigurationName = Release; 698 | }; 699 | 1F6D418B25FDFC9600DF94F5 /* Build configuration list for PBXNativeTarget "MvvmDemoUITests" */ = { 700 | isa = XCConfigurationList; 701 | buildConfigurations = ( 702 | 1F6D418C25FDFC9600DF94F5 /* Debug */, 703 | 1F6D418D25FDFC9600DF94F5 /* Release */, 704 | ); 705 | defaultConfigurationIsVisible = 0; 706 | defaultConfigurationName = Release; 707 | }; 708 | /* End XCConfigurationList section */ 709 | }; 710 | rootObject = 1F6D414E25FDFC9400DF94F5 /* Project object */; 711 | } 712 | --------------------------------------------------------------------------------