├── 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 |
--------------------------------------------------------------------------------