├── .gitignore ├── .gitmodules ├── FinishedApp.png ├── Podfile ├── Podfile.lock ├── README.md ├── ReactiveSwiftFlickrSearch.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ReactiveSwiftFlickrSearch.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── ReactiveSwiftFlickrSearch.xccheckout ├── ReactiveSwiftFlickrSearch ├── AppDelegate.swift ├── BridgingHeader.h ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist ├── Model │ ├── FlickrPhoto.swift │ ├── FlickrPhotoMetadata.swift │ ├── FlickrSearch.swift │ ├── FlickrSearchImpl.swift │ └── FlickrSearchResult.swift ├── PreviousSearchViewModel.swift ├── SearchResultsItemViewModel.swift ├── Util │ ├── RAC.swift │ ├── RACObserve.swift │ ├── RACSignal+Extensions.swift │ └── TableViewBindingHelper.swift ├── View │ ├── FlickrSearchViewController.swift │ ├── FlickrSearchViewController.xib │ ├── RecentSearchItemTableViewCell.swift │ ├── RecentSearchItemTableViewCell.xib │ ├── SearchResultsTableViewCell.swift │ ├── SearchResultsTableViewCell.xib │ ├── SearchResultsViewController.swift │ ├── SearchResultsViewController.xib │ ├── comment.png │ └── fave.png ├── ViewModel │ ├── FlickrSearchViewModel.swift │ ├── SearchResultsViewModel.swift │ └── ViewModelServices.swift └── ViewModelServicesImpl.swift └── ReactiveSwiftFlickrSearchTests ├── Info.plist └── ReactiveSwiftFlickrSearchTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ######################### 2 | # .gitignore file for Xcode4 / OS X Source projects 3 | # 4 | # Version 2.0 5 | # For latest version, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 6 | # 7 | # 2013 updates: 8 | # - fixed the broken "save personal Schemes" 9 | # 10 | # NB: if you are storing "built" products, this WILL NOT WORK, 11 | # and you should use a different .gitignore (or none at all) 12 | # This file is for SOURCE projects, where there are many extra 13 | # files that we want to exclude 14 | # 15 | ######################### 16 | 17 | ##### 18 | # OS X temporary files that should never be committed 19 | 20 | .DS_Store 21 | *.swp 22 | *.lock 23 | profile 24 | 25 | 26 | #### 27 | # Xcode temporary files that should never be committed 28 | # 29 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 30 | 31 | *~.nib 32 | 33 | 34 | #### 35 | # Xcode build files - 36 | # 37 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 38 | 39 | DerivedData/ 40 | 41 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 42 | 43 | build/ 44 | 45 | 46 | ##### 47 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 48 | # 49 | # This is complicated: 50 | # 51 | # SOMETIMES you need to put this file in version control. 52 | # Apple designed it poorly - if you use "custom executables", they are 53 | # saved in this file. 54 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 55 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 56 | 57 | *.pbxuser 58 | *.mode1v3 59 | *.mode2v3 60 | *.perspectivev3 61 | # NB: also, whitelist the default ones, some projects need to use these 62 | !default.pbxuser 63 | !default.mode1v3 64 | !default.mode2v3 65 | !default.perspectivev3 66 | 67 | 68 | #### 69 | # Xcode 4 - semi-personal settings 70 | # 71 | # 72 | # OPTION 1: --------------------------------- 73 | # throw away ALL personal settings (including custom schemes! 74 | # - unless they are "shared") 75 | # 76 | # NB: this is exclusive with OPTION 2 below 77 | xcuserdata 78 | *.xcscmblueprint 79 | 80 | # OPTION 2: --------------------------------- 81 | # get rid of ALL personal settings, but KEEP SOME OF THEM 82 | # - NB: you must manually uncomment the bits you want to keep 83 | # 84 | # NB: this is exclusive with OPTION 1 above 85 | # 86 | #xcuserdata/**/* 87 | 88 | # (requires option 2 above): Personal Schemes 89 | # 90 | #!xcuserdata/**/xcschemes/* 91 | 92 | #### 93 | # XCode 4 workspaces - more detailed 94 | # 95 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 96 | # 97 | # Workspace layout is quite spammy. For reference: 98 | # 99 | # /(root)/ 100 | # /(project-name).xcodeproj/ 101 | # project.pbxproj 102 | # /project.xcworkspace/ 103 | # contents.xcworkspacedata 104 | # /xcuserdata/ 105 | # /(your name)/xcuserdatad/ 106 | # UserInterfaceState.xcuserstate 107 | # /xcsshareddata/ 108 | # /xcschemes/ 109 | # (shared scheme name).xcscheme 110 | # /xcuserdata/ 111 | # /(your name)/xcuserdatad/ 112 | # (private scheme).xcscheme 113 | # xcschememanagement.plist 114 | # 115 | # 116 | 117 | #### 118 | # Xcode 4 - Deprecated classes 119 | # 120 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 121 | # 122 | # We're using source-control, so this is a "feature" that we do not want! 123 | 124 | *.moved-aside 125 | 126 | #### 127 | # Cocoapods: cocoapods.org 128 | # 129 | # Ignoring these files means that whoever uses the code will first have to run: 130 | # pod install 131 | # in the App.xcodeproj directory. 132 | # This ensures the latest dependencies are used. 133 | Pods/ 134 | #Podfile.lock 135 | 136 | 137 | #### 138 | # UNKNOWN: recommended by others, but I can't discover what these files are 139 | # 140 | # ...none. Everything is now explained. 141 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinEberhardt/ReactiveSwiftFlickrSearch/39306508d8d2facc862c18f7545600cd12afcc73/.gitmodules -------------------------------------------------------------------------------- /FinishedApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinEberhardt/ReactiveSwiftFlickrSearch/39306508d8d2facc862c18f7545600cd12afcc73/FinishedApp.png -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '8.0' 2 | use_frameworks! 3 | 4 | target "ReactiveSwiftFlickrSearch" do 5 | pod 'Box', '2.0' 6 | pod 'objectiveflickr', '2.0.4' 7 | pod 'SDWebImage' 8 | pod 'ReactiveCocoa', '4.0.1' 9 | end -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Box (2.0) 3 | - objectiveflickr (2.0.4) 4 | - ReactiveCocoa (4.0.1): 5 | - ReactiveCocoa/UI (= 4.0.1) 6 | - Result (~> 1.0.2) 7 | - ReactiveCocoa/Core (4.0.1): 8 | - ReactiveCocoa/no-arc 9 | - Result (~> 1.0.2) 10 | - ReactiveCocoa/no-arc (4.0.1): 11 | - Result (~> 1.0.2) 12 | - ReactiveCocoa/UI (4.0.1): 13 | - ReactiveCocoa/Core 14 | - Result (~> 1.0.2) 15 | - Result (1.0.2) 16 | - SDWebImage (3.7.5): 17 | - SDWebImage/Core (= 3.7.5) 18 | - SDWebImage/Core (3.7.5) 19 | 20 | DEPENDENCIES: 21 | - Box (= 2.0) 22 | - objectiveflickr (= 2.0.4) 23 | - ReactiveCocoa (= 4.0.1) 24 | - SDWebImage 25 | 26 | SPEC CHECKSUMS: 27 | Box: 70713ca3679e4e11ad4c470e9b58265f506fc659 28 | objectiveflickr: aed169dae1eb5a5550ca78f7efd7ccde2e4c26e5 29 | ReactiveCocoa: f7011630aee4deeb16352fcb1b50d6bde9db4f90 30 | Result: dd3dd71af3fa2e262f1a999e14fba2c25ec14f16 31 | SDWebImage: 69c6303e3348fba97e03f65d65d4fbc26740f461 32 | 33 | COCOAPODS: 0.39.0 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactiveCocoa, Swift and MVVM 2 | 3 | This application is a Swift-port of an MVVM / ReactiveCocoa example application [I wrote a few months ago](https://github.com/ColinEberhardt/ReactiveFlickrSearch). 4 | 5 | ![image](FinishedApp.png) 6 | 7 | ## Instructions 8 | 9 | This project uses a combination of both CocoaPods and git submodules. The reason for this is that the current version of ReactiveCocoa is not compatible with Swift, as [detailed in this blog post](http://www.scottlogic.com/blog/2014/07/24/mvvm-reactivecocoa-swift.html). 10 | 11 | This project uses CocoaPods, therefore to get started you must first install the dependencies: 12 | 13 | pod install 14 | 15 | Open the workspace in Xcode, and select the ReactiveSwiftFlickrSearch target, build and run. 16 | 17 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3C0E601319758A2B0009D5B2 /* FlickrSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E601219758A2B0009D5B2 /* FlickrSearchViewController.swift */; }; 11 | 3C0E601E19758F060009D5B2 /* FlickrSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E601D19758F060009D5B2 /* FlickrSearchViewModel.swift */; }; 12 | 3C0E60221975B3930009D5B2 /* FlickrSearchViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3C0E60211975B3930009D5B2 /* FlickrSearchViewController.xib */; }; 13 | 3C0E60241975B7160009D5B2 /* RAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E60231975B7160009D5B2 /* RAC.swift */; }; 14 | 3C0E60261975BA670009D5B2 /* RACObserve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E60251975BA670009D5B2 /* RACObserve.swift */; }; 15 | 3C0E602A1975D2770009D5B2 /* ViewModelServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E60291975D2770009D5B2 /* ViewModelServices.swift */; }; 16 | 3C0E602C1975D2B70009D5B2 /* ViewModelServicesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E602B1975D2B70009D5B2 /* ViewModelServicesImpl.swift */; }; 17 | 3C0E602E1975D33C0009D5B2 /* SearchResultsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3C0E602D1975D33C0009D5B2 /* SearchResultsViewController.xib */; }; 18 | 3C0E60301975D3500009D5B2 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0E602F1975D3500009D5B2 /* SearchResultsViewController.swift */; }; 19 | 3C1497231975D730002548CC /* SearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1497221975D730002548CC /* SearchResultsViewModel.swift */; }; 20 | 3C14972C1975D9BB002548CC /* TableViewBindingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14972B1975D9BB002548CC /* TableViewBindingHelper.swift */; }; 21 | 3C9E38481978FC0A005BFD1A /* FlickrPhotoMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E38471978FC0A005BFD1A /* FlickrPhotoMetadata.swift */; }; 22 | 3C9E38511978FF3C005BFD1A /* SearchResultsItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9E38501978FF3C005BFD1A /* SearchResultsItemViewModel.swift */; }; 23 | 3CC6ABF31975065D008F0262 /* FlickrPhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6ABEF1975065D008F0262 /* FlickrPhoto.swift */; }; 24 | 3CC6ABF41975065D008F0262 /* FlickrSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6ABF01975065D008F0262 /* FlickrSearch.swift */; }; 25 | 3CC6ABF51975065D008F0262 /* FlickrSearchImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6ABF11975065D008F0262 /* FlickrSearchImpl.swift */; }; 26 | 3CC6ABF61975065D008F0262 /* FlickrSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6ABF21975065D008F0262 /* FlickrSearchResult.swift */; }; 27 | 3CC6ABF919750696008F0262 /* RACSignal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6ABF819750696008F0262 /* RACSignal+Extensions.swift */; }; 28 | 3CE213DD1969B9E700CE9B0B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE213DC1969B9E700CE9B0B /* AppDelegate.swift */; }; 29 | 3CE213E41969B9E700CE9B0B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3CE213E31969B9E700CE9B0B /* Images.xcassets */; }; 30 | 3CE213F01969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE213EF1969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.swift */; }; 31 | 3CF710D219782DB00011554F /* comment.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CF710D019782DB00011554F /* comment.png */; }; 32 | 3CF710D319782DB00011554F /* fave.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CF710D119782DB00011554F /* fave.png */; }; 33 | 3CF973DD19765B01003F00E9 /* SearchResultsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3CF973DC19765B01003F00E9 /* SearchResultsTableViewCell.xib */; }; 34 | 3CF973DF19765CFF003F00E9 /* SearchResultsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF973DE19765CFF003F00E9 /* SearchResultsTableViewCell.swift */; }; 35 | 3CF973E119766188003F00E9 /* PreviousSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF973E019766188003F00E9 /* PreviousSearchViewModel.swift */; }; 36 | 3CF973E3197665DE003F00E9 /* RecentSearchItemTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3CF973E2197665DE003F00E9 /* RecentSearchItemTableViewCell.xib */; }; 37 | 3CF973E5197665F5003F00E9 /* RecentSearchItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF973E4197665F5003F00E9 /* RecentSearchItemTableViewCell.swift */; }; 38 | 8B229D9315106A1748354AF5 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B41A463FFDDD1C5CEBB89397 /* Pods.framework */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | 3CE213EA1969B9E800CE9B0B /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 3CE213CF1969B9E700CE9B0B /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 3CE213D61969B9E700CE9B0B; 47 | remoteInfo = ReactiveSwiftFlickrSearch; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 0E3C9E68A90FC5A36C5BA6B3 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; 53 | 3C0E601219758A2B0009D5B2 /* FlickrSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrSearchViewController.swift; sourceTree = ""; }; 54 | 3C0E601D19758F060009D5B2 /* FlickrSearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FlickrSearchViewModel.swift; path = ViewModel/FlickrSearchViewModel.swift; sourceTree = ""; }; 55 | 3C0E60211975B3930009D5B2 /* FlickrSearchViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FlickrSearchViewController.xib; sourceTree = ""; }; 56 | 3C0E60231975B7160009D5B2 /* RAC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RAC.swift; path = Util/RAC.swift; sourceTree = ""; }; 57 | 3C0E60251975BA670009D5B2 /* RACObserve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RACObserve.swift; path = Util/RACObserve.swift; sourceTree = ""; }; 58 | 3C0E60291975D2770009D5B2 /* ViewModelServices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewModelServices.swift; path = ViewModel/ViewModelServices.swift; sourceTree = ""; }; 59 | 3C0E602B1975D2B70009D5B2 /* ViewModelServicesImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelServicesImpl.swift; sourceTree = ""; }; 60 | 3C0E602D1975D33C0009D5B2 /* SearchResultsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SearchResultsViewController.xib; sourceTree = ""; }; 61 | 3C0E602F1975D3500009D5B2 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; 62 | 3C1497221975D730002548CC /* SearchResultsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchResultsViewModel.swift; path = ViewModel/SearchResultsViewModel.swift; sourceTree = ""; }; 63 | 3C14972B1975D9BB002548CC /* TableViewBindingHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableViewBindingHelper.swift; path = Util/TableViewBindingHelper.swift; sourceTree = ""; }; 64 | 3C9E38471978FC0A005BFD1A /* FlickrPhotoMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoMetadata.swift; sourceTree = ""; }; 65 | 3C9E38501978FF3C005BFD1A /* SearchResultsItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsItemViewModel.swift; sourceTree = ""; }; 66 | 3CC6ABEF1975065D008F0262 /* FlickrPhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhoto.swift; sourceTree = ""; }; 67 | 3CC6ABF01975065D008F0262 /* FlickrSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrSearch.swift; sourceTree = ""; }; 68 | 3CC6ABF11975065D008F0262 /* FlickrSearchImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrSearchImpl.swift; sourceTree = ""; }; 69 | 3CC6ABF21975065D008F0262 /* FlickrSearchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrSearchResult.swift; sourceTree = ""; }; 70 | 3CC6ABF819750696008F0262 /* RACSignal+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "RACSignal+Extensions.swift"; path = "Util/RACSignal+Extensions.swift"; sourceTree = ""; }; 71 | 3CE213D71969B9E700CE9B0B /* ReactiveSwiftFlickrSearch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactiveSwiftFlickrSearch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 3CE213DB1969B9E700CE9B0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 3CE213DC1969B9E700CE9B0B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 74 | 3CE213E31969B9E700CE9B0B /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 75 | 3CE213E91969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactiveSwiftFlickrSearchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 3CE213EE1969B9E800CE9B0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77 | 3CE213EF1969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactiveSwiftFlickrSearchTests.swift; sourceTree = ""; }; 78 | 3CE21D831969BD7F00CE9B0B /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; 79 | 3CF710D019782DB00011554F /* comment.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = comment.png; sourceTree = ""; }; 80 | 3CF710D119782DB00011554F /* fave.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = fave.png; sourceTree = ""; }; 81 | 3CF973DC19765B01003F00E9 /* SearchResultsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SearchResultsTableViewCell.xib; sourceTree = ""; }; 82 | 3CF973DE19765CFF003F00E9 /* SearchResultsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsTableViewCell.swift; sourceTree = ""; }; 83 | 3CF973E019766188003F00E9 /* PreviousSearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviousSearchViewModel.swift; sourceTree = ""; }; 84 | 3CF973E2197665DE003F00E9 /* RecentSearchItemTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RecentSearchItemTableViewCell.xib; sourceTree = ""; }; 85 | 3CF973E4197665F5003F00E9 /* RecentSearchItemTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentSearchItemTableViewCell.swift; sourceTree = ""; }; 86 | B41A463FFDDD1C5CEBB89397 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87 | FDDADB51CE0922CBF02FFBEA /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; 88 | /* End PBXFileReference section */ 89 | 90 | /* Begin PBXFrameworksBuildPhase section */ 91 | 3CE213D41969B9E700CE9B0B /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 8B229D9315106A1748354AF5 /* Pods.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | 3CE213E61969B9E800CE9B0B /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 190DB24D31AE2A67BCE456CC /* Pods */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 0E3C9E68A90FC5A36C5BA6B3 /* Pods.debug.xcconfig */, 113 | FDDADB51CE0922CBF02FFBEA /* Pods.release.xcconfig */, 114 | ); 115 | name = Pods; 116 | sourceTree = ""; 117 | }; 118 | 3C0E600919757DE30009D5B2 /* View */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 3CF710D019782DB00011554F /* comment.png */, 122 | 3CF710D119782DB00011554F /* fave.png */, 123 | 3C0E60211975B3930009D5B2 /* FlickrSearchViewController.xib */, 124 | 3C0E601219758A2B0009D5B2 /* FlickrSearchViewController.swift */, 125 | 3C0E602D1975D33C0009D5B2 /* SearchResultsViewController.xib */, 126 | 3C0E602F1975D3500009D5B2 /* SearchResultsViewController.swift */, 127 | 3CF973DC19765B01003F00E9 /* SearchResultsTableViewCell.xib */, 128 | 3CF973DE19765CFF003F00E9 /* SearchResultsTableViewCell.swift */, 129 | 3CF973E2197665DE003F00E9 /* RecentSearchItemTableViewCell.xib */, 130 | 3CF973E4197665F5003F00E9 /* RecentSearchItemTableViewCell.swift */, 131 | ); 132 | path = View; 133 | sourceTree = ""; 134 | }; 135 | 3C0E601C19758EEE0009D5B2 /* ViewModel */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 3C0E601D19758F060009D5B2 /* FlickrSearchViewModel.swift */, 139 | 3C0E60291975D2770009D5B2 /* ViewModelServices.swift */, 140 | 3C1497221975D730002548CC /* SearchResultsViewModel.swift */, 141 | 3CF973E019766188003F00E9 /* PreviousSearchViewModel.swift */, 142 | 3C9E38501978FF3C005BFD1A /* SearchResultsItemViewModel.swift */, 143 | ); 144 | name = ViewModel; 145 | sourceTree = ""; 146 | }; 147 | 3CC6ABEE1975065D008F0262 /* Model */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 3CC6ABEF1975065D008F0262 /* FlickrPhoto.swift */, 151 | 3CC6ABF01975065D008F0262 /* FlickrSearch.swift */, 152 | 3CC6ABF11975065D008F0262 /* FlickrSearchImpl.swift */, 153 | 3CC6ABF21975065D008F0262 /* FlickrSearchResult.swift */, 154 | 3C9E38471978FC0A005BFD1A /* FlickrPhotoMetadata.swift */, 155 | ); 156 | path = Model; 157 | sourceTree = ""; 158 | }; 159 | 3CC6ABF719750680008F0262 /* Util */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 3CC6ABF819750696008F0262 /* RACSignal+Extensions.swift */, 163 | 3C0E60231975B7160009D5B2 /* RAC.swift */, 164 | 3C0E60251975BA670009D5B2 /* RACObserve.swift */, 165 | 3C14972B1975D9BB002548CC /* TableViewBindingHelper.swift */, 166 | ); 167 | name = Util; 168 | sourceTree = ""; 169 | }; 170 | 3CE213CE1969B9E700CE9B0B = { 171 | isa = PBXGroup; 172 | children = ( 173 | 3CE213D91969B9E700CE9B0B /* ReactiveSwiftFlickrSearch */, 174 | 3CE213EC1969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests */, 175 | 3CE213D81969B9E700CE9B0B /* Products */, 176 | 5C577225BD6A4C1198E650C5 /* Frameworks */, 177 | 190DB24D31AE2A67BCE456CC /* Pods */, 178 | ); 179 | sourceTree = ""; 180 | }; 181 | 3CE213D81969B9E700CE9B0B /* Products */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 3CE213D71969B9E700CE9B0B /* ReactiveSwiftFlickrSearch.app */, 185 | 3CE213E91969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.xctest */, 186 | ); 187 | name = Products; 188 | sourceTree = ""; 189 | }; 190 | 3CE213D91969B9E700CE9B0B /* ReactiveSwiftFlickrSearch */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 3C0E601C19758EEE0009D5B2 /* ViewModel */, 194 | 3C0E600919757DE30009D5B2 /* View */, 195 | 3CC6ABF719750680008F0262 /* Util */, 196 | 3CC6ABEE1975065D008F0262 /* Model */, 197 | 3CE213DC1969B9E700CE9B0B /* AppDelegate.swift */, 198 | 3CE213E31969B9E700CE9B0B /* Images.xcassets */, 199 | 3CE213DA1969B9E700CE9B0B /* Supporting Files */, 200 | 3CE21D831969BD7F00CE9B0B /* BridgingHeader.h */, 201 | 3C0E602B1975D2B70009D5B2 /* ViewModelServicesImpl.swift */, 202 | ); 203 | path = ReactiveSwiftFlickrSearch; 204 | sourceTree = ""; 205 | }; 206 | 3CE213DA1969B9E700CE9B0B /* Supporting Files */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 3CE213DB1969B9E700CE9B0B /* Info.plist */, 210 | ); 211 | name = "Supporting Files"; 212 | sourceTree = ""; 213 | }; 214 | 3CE213EC1969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 3CE213EF1969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.swift */, 218 | 3CE213ED1969B9E800CE9B0B /* Supporting Files */, 219 | ); 220 | path = ReactiveSwiftFlickrSearchTests; 221 | sourceTree = ""; 222 | }; 223 | 3CE213ED1969B9E800CE9B0B /* Supporting Files */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 3CE213EE1969B9E800CE9B0B /* Info.plist */, 227 | ); 228 | name = "Supporting Files"; 229 | sourceTree = ""; 230 | }; 231 | 5C577225BD6A4C1198E650C5 /* Frameworks */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | B41A463FFDDD1C5CEBB89397 /* Pods.framework */, 235 | ); 236 | name = Frameworks; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXGroup section */ 240 | 241 | /* Begin PBXNativeTarget section */ 242 | 3CE213D61969B9E700CE9B0B /* ReactiveSwiftFlickrSearch */ = { 243 | isa = PBXNativeTarget; 244 | buildConfigurationList = 3CE213F31969B9E800CE9B0B /* Build configuration list for PBXNativeTarget "ReactiveSwiftFlickrSearch" */; 245 | buildPhases = ( 246 | 04FFC06EDD674E188E90C0D3 /* Check Pods Manifest.lock */, 247 | 3CE213D31969B9E700CE9B0B /* Sources */, 248 | 3CE213D41969B9E700CE9B0B /* Frameworks */, 249 | 3CE213D51969B9E700CE9B0B /* Resources */, 250 | 18290FAB1FA049988228338F /* Copy Pods Resources */, 251 | 9FF2CA13358533156DD64E5E /* Embed Pods Frameworks */, 252 | ); 253 | buildRules = ( 254 | ); 255 | dependencies = ( 256 | ); 257 | name = ReactiveSwiftFlickrSearch; 258 | productName = ReactiveSwiftFlickrSearch; 259 | productReference = 3CE213D71969B9E700CE9B0B /* ReactiveSwiftFlickrSearch.app */; 260 | productType = "com.apple.product-type.application"; 261 | }; 262 | 3CE213E81969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 3CE213F61969B9E800CE9B0B /* Build configuration list for PBXNativeTarget "ReactiveSwiftFlickrSearchTests" */; 265 | buildPhases = ( 266 | 3CE213E51969B9E800CE9B0B /* Sources */, 267 | 3CE213E61969B9E800CE9B0B /* Frameworks */, 268 | 3CE213E71969B9E800CE9B0B /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | 3CE213EB1969B9E800CE9B0B /* PBXTargetDependency */, 274 | ); 275 | name = ReactiveSwiftFlickrSearchTests; 276 | productName = ReactiveSwiftFlickrSearchTests; 277 | productReference = 3CE213E91969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.xctest */; 278 | productType = "com.apple.product-type.bundle.unit-test"; 279 | }; 280 | /* End PBXNativeTarget section */ 281 | 282 | /* Begin PBXProject section */ 283 | 3CE213CF1969B9E700CE9B0B /* Project object */ = { 284 | isa = PBXProject; 285 | attributes = { 286 | LastSwiftMigration = 0720; 287 | LastSwiftUpdateCheck = 0720; 288 | LastUpgradeCheck = 0720; 289 | ORGANIZATIONNAME = "Colin Eberhardt"; 290 | TargetAttributes = { 291 | 3CE213D61969B9E700CE9B0B = { 292 | CreatedOnToolsVersion = 6.0; 293 | }; 294 | 3CE213E81969B9E800CE9B0B = { 295 | CreatedOnToolsVersion = 6.0; 296 | TestTargetID = 3CE213D61969B9E700CE9B0B; 297 | }; 298 | }; 299 | }; 300 | buildConfigurationList = 3CE213D21969B9E700CE9B0B /* Build configuration list for PBXProject "ReactiveSwiftFlickrSearch" */; 301 | compatibilityVersion = "Xcode 3.2"; 302 | developmentRegion = English; 303 | hasScannedForEncodings = 0; 304 | knownRegions = ( 305 | en, 306 | Base, 307 | ); 308 | mainGroup = 3CE213CE1969B9E700CE9B0B; 309 | productRefGroup = 3CE213D81969B9E700CE9B0B /* Products */; 310 | projectDirPath = ""; 311 | projectRoot = ""; 312 | targets = ( 313 | 3CE213D61969B9E700CE9B0B /* ReactiveSwiftFlickrSearch */, 314 | 3CE213E81969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests */, 315 | ); 316 | }; 317 | /* End PBXProject section */ 318 | 319 | /* Begin PBXResourcesBuildPhase section */ 320 | 3CE213D51969B9E700CE9B0B /* Resources */ = { 321 | isa = PBXResourcesBuildPhase; 322 | buildActionMask = 2147483647; 323 | files = ( 324 | 3C0E60221975B3930009D5B2 /* FlickrSearchViewController.xib in Resources */, 325 | 3CF973E3197665DE003F00E9 /* RecentSearchItemTableViewCell.xib in Resources */, 326 | 3CE213E41969B9E700CE9B0B /* Images.xcassets in Resources */, 327 | 3CF973DD19765B01003F00E9 /* SearchResultsTableViewCell.xib in Resources */, 328 | 3CF710D319782DB00011554F /* fave.png in Resources */, 329 | 3C0E602E1975D33C0009D5B2 /* SearchResultsViewController.xib in Resources */, 330 | 3CF710D219782DB00011554F /* comment.png in Resources */, 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | }; 334 | 3CE213E71969B9E800CE9B0B /* Resources */ = { 335 | isa = PBXResourcesBuildPhase; 336 | buildActionMask = 2147483647; 337 | files = ( 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | /* End PBXResourcesBuildPhase section */ 342 | 343 | /* Begin PBXShellScriptBuildPhase section */ 344 | 04FFC06EDD674E188E90C0D3 /* Check Pods Manifest.lock */ = { 345 | isa = PBXShellScriptBuildPhase; 346 | buildActionMask = 2147483647; 347 | files = ( 348 | ); 349 | inputPaths = ( 350 | ); 351 | name = "Check Pods Manifest.lock"; 352 | outputPaths = ( 353 | ); 354 | runOnlyForDeploymentPostprocessing = 0; 355 | shellPath = /bin/sh; 356 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 357 | showEnvVarsInLog = 0; 358 | }; 359 | 18290FAB1FA049988228338F /* Copy Pods Resources */ = { 360 | isa = PBXShellScriptBuildPhase; 361 | buildActionMask = 2147483647; 362 | files = ( 363 | ); 364 | inputPaths = ( 365 | ); 366 | name = "Copy Pods Resources"; 367 | outputPaths = ( 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | shellPath = /bin/sh; 371 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; 372 | showEnvVarsInLog = 0; 373 | }; 374 | 9FF2CA13358533156DD64E5E /* Embed Pods Frameworks */ = { 375 | isa = PBXShellScriptBuildPhase; 376 | buildActionMask = 2147483647; 377 | files = ( 378 | ); 379 | inputPaths = ( 380 | ); 381 | name = "Embed Pods Frameworks"; 382 | outputPaths = ( 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | shellPath = /bin/sh; 386 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; 387 | showEnvVarsInLog = 0; 388 | }; 389 | /* End PBXShellScriptBuildPhase section */ 390 | 391 | /* Begin PBXSourcesBuildPhase section */ 392 | 3CE213D31969B9E700CE9B0B /* Sources */ = { 393 | isa = PBXSourcesBuildPhase; 394 | buildActionMask = 2147483647; 395 | files = ( 396 | 3CC6ABF31975065D008F0262 /* FlickrPhoto.swift in Sources */, 397 | 3C9E38511978FF3C005BFD1A /* SearchResultsItemViewModel.swift in Sources */, 398 | 3CF973E119766188003F00E9 /* PreviousSearchViewModel.swift in Sources */, 399 | 3CC6ABF919750696008F0262 /* RACSignal+Extensions.swift in Sources */, 400 | 3CF973DF19765CFF003F00E9 /* SearchResultsTableViewCell.swift in Sources */, 401 | 3CC6ABF41975065D008F0262 /* FlickrSearch.swift in Sources */, 402 | 3CC6ABF61975065D008F0262 /* FlickrSearchResult.swift in Sources */, 403 | 3C0E601E19758F060009D5B2 /* FlickrSearchViewModel.swift in Sources */, 404 | 3C14972C1975D9BB002548CC /* TableViewBindingHelper.swift in Sources */, 405 | 3C9E38481978FC0A005BFD1A /* FlickrPhotoMetadata.swift in Sources */, 406 | 3C0E60301975D3500009D5B2 /* SearchResultsViewController.swift in Sources */, 407 | 3C0E60261975BA670009D5B2 /* RACObserve.swift in Sources */, 408 | 3C0E602A1975D2770009D5B2 /* ViewModelServices.swift in Sources */, 409 | 3CE213DD1969B9E700CE9B0B /* AppDelegate.swift in Sources */, 410 | 3C0E60241975B7160009D5B2 /* RAC.swift in Sources */, 411 | 3C0E601319758A2B0009D5B2 /* FlickrSearchViewController.swift in Sources */, 412 | 3C0E602C1975D2B70009D5B2 /* ViewModelServicesImpl.swift in Sources */, 413 | 3CC6ABF51975065D008F0262 /* FlickrSearchImpl.swift in Sources */, 414 | 3CF973E5197665F5003F00E9 /* RecentSearchItemTableViewCell.swift in Sources */, 415 | 3C1497231975D730002548CC /* SearchResultsViewModel.swift in Sources */, 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | 3CE213E51969B9E800CE9B0B /* Sources */ = { 420 | isa = PBXSourcesBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | 3CE213F01969B9E800CE9B0B /* ReactiveSwiftFlickrSearchTests.swift in Sources */, 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | /* End PBXSourcesBuildPhase section */ 428 | 429 | /* Begin PBXTargetDependency section */ 430 | 3CE213EB1969B9E800CE9B0B /* PBXTargetDependency */ = { 431 | isa = PBXTargetDependency; 432 | target = 3CE213D61969B9E700CE9B0B /* ReactiveSwiftFlickrSearch */; 433 | targetProxy = 3CE213EA1969B9E800CE9B0B /* PBXContainerItemProxy */; 434 | }; 435 | /* End PBXTargetDependency section */ 436 | 437 | /* Begin XCBuildConfiguration section */ 438 | 3CE213F11969B9E800CE9B0B /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_SEARCH_USER_PATHS = NO; 442 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 443 | CLANG_CXX_LIBRARY = "libc++"; 444 | CLANG_ENABLE_MODULES = YES; 445 | CLANG_ENABLE_OBJC_ARC = YES; 446 | CLANG_WARN_BOOL_CONVERSION = YES; 447 | CLANG_WARN_CONSTANT_CONVERSION = YES; 448 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 449 | CLANG_WARN_EMPTY_BODY = YES; 450 | CLANG_WARN_ENUM_CONVERSION = YES; 451 | CLANG_WARN_INT_CONVERSION = YES; 452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 453 | CLANG_WARN_UNREACHABLE_CODE = YES; 454 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 455 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 456 | COPY_PHASE_STRIP = NO; 457 | ENABLE_STRICT_OBJC_MSGSEND = YES; 458 | ENABLE_TESTABILITY = YES; 459 | GCC_C_LANGUAGE_STANDARD = gnu99; 460 | GCC_DYNAMIC_NO_PIC = NO; 461 | GCC_OPTIMIZATION_LEVEL = 0; 462 | GCC_PREPROCESSOR_DEFINITIONS = ( 463 | "DEBUG=1", 464 | "$(inherited)", 465 | ); 466 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 467 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 468 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 469 | GCC_WARN_UNDECLARED_SELECTOR = YES; 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 471 | GCC_WARN_UNUSED_FUNCTION = YES; 472 | GCC_WARN_UNUSED_VARIABLE = YES; 473 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 474 | METAL_ENABLE_DEBUG_INFO = YES; 475 | ONLY_ACTIVE_ARCH = YES; 476 | SDKROOT = iphoneos; 477 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | }; 480 | name = Debug; 481 | }; 482 | 3CE213F21969B9E800CE9B0B /* Release */ = { 483 | isa = XCBuildConfiguration; 484 | buildSettings = { 485 | ALWAYS_SEARCH_USER_PATHS = NO; 486 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 487 | CLANG_CXX_LIBRARY = "libc++"; 488 | CLANG_ENABLE_MODULES = YES; 489 | CLANG_ENABLE_OBJC_ARC = YES; 490 | CLANG_WARN_BOOL_CONVERSION = YES; 491 | CLANG_WARN_CONSTANT_CONVERSION = YES; 492 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 493 | CLANG_WARN_EMPTY_BODY = YES; 494 | CLANG_WARN_ENUM_CONVERSION = YES; 495 | CLANG_WARN_INT_CONVERSION = YES; 496 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 497 | CLANG_WARN_UNREACHABLE_CODE = YES; 498 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 499 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 500 | COPY_PHASE_STRIP = YES; 501 | ENABLE_NS_ASSERTIONS = NO; 502 | ENABLE_STRICT_OBJC_MSGSEND = YES; 503 | GCC_C_LANGUAGE_STANDARD = gnu99; 504 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 505 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 506 | GCC_WARN_UNDECLARED_SELECTOR = YES; 507 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 508 | GCC_WARN_UNUSED_FUNCTION = YES; 509 | GCC_WARN_UNUSED_VARIABLE = YES; 510 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 511 | METAL_ENABLE_DEBUG_INFO = NO; 512 | SDKROOT = iphoneos; 513 | TARGETED_DEVICE_FAMILY = "1,2"; 514 | VALIDATE_PRODUCT = YES; 515 | }; 516 | name = Release; 517 | }; 518 | 3CE213F41969B9E800CE9B0B /* Debug */ = { 519 | isa = XCBuildConfiguration; 520 | baseConfigurationReference = 0E3C9E68A90FC5A36C5BA6B3 /* Pods.debug.xcconfig */; 521 | buildSettings = { 522 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 523 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 524 | INFOPLIST_FILE = ReactiveSwiftFlickrSearch/Info.plist; 525 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 527 | LIBRARY_SEARCH_PATHS = ( 528 | "$(inherited)", 529 | "$(PROJECT_DIR)/ReactiveCocoa/external/specta/test/vendor/expecta", 530 | ); 531 | PRODUCT_BUNDLE_IDENTIFIER = "com.scottlogic.${PRODUCT_NAME:rfc1034identifier}"; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ReactiveSwiftFlickrSearch/BridgingHeader.h"; 534 | }; 535 | name = Debug; 536 | }; 537 | 3CE213F51969B9E800CE9B0B /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | baseConfigurationReference = FDDADB51CE0922CBF02FFBEA /* Pods.release.xcconfig */; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 543 | INFOPLIST_FILE = ReactiveSwiftFlickrSearch/Info.plist; 544 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 545 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 546 | LIBRARY_SEARCH_PATHS = ( 547 | "$(inherited)", 548 | "$(PROJECT_DIR)/ReactiveCocoa/external/specta/test/vendor/expecta", 549 | ); 550 | PRODUCT_BUNDLE_IDENTIFIER = "com.scottlogic.${PRODUCT_NAME:rfc1034identifier}"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ReactiveSwiftFlickrSearch/BridgingHeader.h"; 553 | }; 554 | name = Release; 555 | }; 556 | 3CE213F71969B9E800CE9B0B /* Debug */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ReactiveSwiftFlickrSearch.app/ReactiveSwiftFlickrSearch"; 560 | FRAMEWORK_SEARCH_PATHS = ( 561 | "$(SDKROOT)/Developer/Library/Frameworks", 562 | "$(inherited)", 563 | ); 564 | GCC_PREPROCESSOR_DEFINITIONS = ( 565 | "DEBUG=1", 566 | "$(inherited)", 567 | ); 568 | INFOPLIST_FILE = ReactiveSwiftFlickrSearchTests/Info.plist; 569 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 570 | METAL_ENABLE_DEBUG_INFO = YES; 571 | PRODUCT_BUNDLE_IDENTIFIER = "com.scottlogic.${PRODUCT_NAME:rfc1034identifier}"; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | TEST_HOST = "$(BUNDLE_LOADER)"; 574 | }; 575 | name = Debug; 576 | }; 577 | 3CE213F81969B9E800CE9B0B /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ReactiveSwiftFlickrSearch.app/ReactiveSwiftFlickrSearch"; 581 | FRAMEWORK_SEARCH_PATHS = ( 582 | "$(SDKROOT)/Developer/Library/Frameworks", 583 | "$(inherited)", 584 | ); 585 | INFOPLIST_FILE = ReactiveSwiftFlickrSearchTests/Info.plist; 586 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 587 | METAL_ENABLE_DEBUG_INFO = NO; 588 | PRODUCT_BUNDLE_IDENTIFIER = "com.scottlogic.${PRODUCT_NAME:rfc1034identifier}"; 589 | PRODUCT_NAME = "$(TARGET_NAME)"; 590 | TEST_HOST = "$(BUNDLE_LOADER)"; 591 | }; 592 | name = Release; 593 | }; 594 | /* End XCBuildConfiguration section */ 595 | 596 | /* Begin XCConfigurationList section */ 597 | 3CE213D21969B9E700CE9B0B /* Build configuration list for PBXProject "ReactiveSwiftFlickrSearch" */ = { 598 | isa = XCConfigurationList; 599 | buildConfigurations = ( 600 | 3CE213F11969B9E800CE9B0B /* Debug */, 601 | 3CE213F21969B9E800CE9B0B /* Release */, 602 | ); 603 | defaultConfigurationIsVisible = 0; 604 | defaultConfigurationName = Release; 605 | }; 606 | 3CE213F31969B9E800CE9B0B /* Build configuration list for PBXNativeTarget "ReactiveSwiftFlickrSearch" */ = { 607 | isa = XCConfigurationList; 608 | buildConfigurations = ( 609 | 3CE213F41969B9E800CE9B0B /* Debug */, 610 | 3CE213F51969B9E800CE9B0B /* Release */, 611 | ); 612 | defaultConfigurationIsVisible = 0; 613 | defaultConfigurationName = Release; 614 | }; 615 | 3CE213F61969B9E800CE9B0B /* Build configuration list for PBXNativeTarget "ReactiveSwiftFlickrSearchTests" */ = { 616 | isa = XCConfigurationList; 617 | buildConfigurations = ( 618 | 3CE213F71969B9E800CE9B0B /* Debug */, 619 | 3CE213F81969B9E800CE9B0B /* Release */, 620 | ); 621 | defaultConfigurationIsVisible = 0; 622 | defaultConfigurationName = Release; 623 | }; 624 | /* End XCConfigurationList section */ 625 | }; 626 | rootObject = 3CE213CF1969B9E700CE9B0B /* Project object */; 627 | } 628 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch.xcworkspace/xcshareddata/ReactiveSwiftFlickrSearch.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 6307F97F-D48E-4985-B266-58E46C5E8C2D 9 | IDESourceControlProjectName 10 | ReactiveSwiftFlickrSearch 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 2FD0F96481B39E576130E173BF6923CF52C91FF7 14 | git://github.com/github/specta.git 15 | 51B210D0D41486D3ADD6940D57E4480BBD4DE4C5 16 | https://github.com/yusefnapora/ReactiveCocoa.git 17 | 8884BD3B878631A1500EC9895E6A3CF4CB0AB3DB 18 | git://github.com/github/expecta.git 19 | 9E3183DE297D66F5206118DE938F0CE0ACB16047 20 | https://github.com/ColinEberhardt/ReactiveSwiftFlickrSearch.git 21 | E084C86B03F81D63323C9E7510697EA528A758C7 22 | https://github.com/jspahrsummers/xcconfigs.git 23 | 24 | IDESourceControlProjectPath 25 | ReactiveSwiftFlickrSearch.xcworkspace 26 | IDESourceControlProjectRelativeInstallPathDictionary 27 | 28 | 2FD0F96481B39E576130E173BF6923CF52C91FF7 29 | ..ReactiveCocoa/external/specta/ 30 | 51B210D0D41486D3ADD6940D57E4480BBD4DE4C5 31 | ..ReactiveCocoa/ 32 | 8884BD3B878631A1500EC9895E6A3CF4CB0AB3DB 33 | ..ReactiveCocoa/external/expecta/ 34 | 9E3183DE297D66F5206118DE938F0CE0ACB16047 35 | .. 36 | E084C86B03F81D63323C9E7510697EA528A758C7 37 | ..ReactiveCocoa/external/xcconfigs/ 38 | 39 | IDESourceControlProjectURL 40 | https://github.com/ColinEberhardt/ReactiveSwiftFlickrSearch.git 41 | IDESourceControlProjectVersion 42 | 111 43 | IDESourceControlProjectWCCIdentifier 44 | 9E3183DE297D66F5206118DE938F0CE0ACB16047 45 | IDESourceControlProjectWCConfigurations 46 | 47 | 48 | IDESourceControlRepositoryExtensionIdentifierKey 49 | public.vcs.git 50 | IDESourceControlWCCIdentifierKey 51 | 8884BD3B878631A1500EC9895E6A3CF4CB0AB3DB 52 | IDESourceControlWCCName 53 | expecta 54 | 55 | 56 | IDESourceControlRepositoryExtensionIdentifierKey 57 | public.vcs.git 58 | IDESourceControlWCCIdentifierKey 59 | 51B210D0D41486D3ADD6940D57E4480BBD4DE4C5 60 | IDESourceControlWCCName 61 | ReactiveCocoa 62 | 63 | 64 | IDESourceControlRepositoryExtensionIdentifierKey 65 | public.vcs.git 66 | IDESourceControlWCCIdentifierKey 67 | 9E3183DE297D66F5206118DE938F0CE0ACB16047 68 | IDESourceControlWCCName 69 | ReactiveSwiftFlickrSearch 70 | 71 | 72 | IDESourceControlRepositoryExtensionIdentifierKey 73 | public.vcs.git 74 | IDESourceControlWCCIdentifierKey 75 | 2FD0F96481B39E576130E173BF6923CF52C91FF7 76 | IDESourceControlWCCName 77 | specta 78 | 79 | 80 | IDESourceControlRepositoryExtensionIdentifierKey 81 | public.vcs.git 82 | IDESourceControlWCCIdentifierKey 83 | E084C86B03F81D63323C9E7510697EA528A758C7 84 | IDESourceControlWCCName 85 | xcconfigs 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 06/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | var navigationController: UINavigationController! 17 | 18 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { 19 | navigationController = UINavigationController() 20 | navigationController.navigationBar.barTintColor = UIColor.darkGrayColor() 21 | navigationController.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.whiteColor()] 22 | 23 | let viewModelServices = ViewModelServicesImpl(navigationController: navigationController) 24 | 25 | let viewModel = FlickrSearchViewModel(services: viewModelServices) 26 | let viewController = FlickrSearchViewController(viewModel: viewModel) 27 | navigationController.pushViewController(viewController, animated: false) 28 | 29 | 30 | window = UIWindow(frame: UIScreen.mainScreen().bounds) 31 | window!.rootViewController = navigationController 32 | window!.makeKeyAndVisible() 33 | 34 | return true 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/BridgingHeader.h: -------------------------------------------------------------------------------- 1 | // 2 | // BridgingHeader.h 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 06/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "1x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Model/FlickrPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrPhoto.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 14/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // represents a single photo in a Flickr search 12 | class FlickrPhoto { 13 | 14 | let title :String 15 | let url :NSURL 16 | let identifier :String 17 | 18 | init (title: String, url: NSURL, identifier: String) { 19 | self.title = title 20 | self.url = url 21 | self.identifier = identifier 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Model/FlickrPhotoMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrPhotoMetadata.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 18/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Represents the metadata relating to a single photo 12 | class FlickrPhotoMetadata { 13 | let favourites: Int 14 | let comments: Int 15 | 16 | init(favourites:Int?, comments: Int?) { 17 | self.favourites = favourites != nil ? favourites! : 0 18 | self.comments = comments != nil ? comments! : 0 19 | } 20 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Model/FlickrSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrSearch.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 14/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | // Provides an API for searching Flickr 13 | protocol FlickrSearch { 14 | 15 | // searches Flickr for the given string, returning a signal that emits the response 16 | func flickrSearchSignal(searchString: String) -> RACSignal 17 | 18 | // searches Flickr for the given photo metadata, returning a signal that emits the response 19 | func flickrImageMetadata(photoId: String) -> RACSignal 20 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Model/FlickrSearchImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrSearchImpl.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 14/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | import objectiveflickr 12 | 13 | // An implementation of the FlickrSearch protocol 14 | class FlickrSearchImpl : NSObject, FlickrSearch, OFFlickrAPIRequestDelegate { 15 | 16 | //MARK: Properties 17 | 18 | private let requests: NSMutableSet 19 | private let flickrContext: OFFlickrAPIContext 20 | private var flickrRequest: OFFlickrAPIRequest? 21 | 22 | //MARK: Public API 23 | 24 | override init() { 25 | let flickrAPIKey = "9d1bdbde083bc30ebe168a64aac50be5"; 26 | let flickrAPISharedSecret = "5fbfa610234c6c23"; 27 | flickrContext = OFFlickrAPIContext(APIKey: flickrAPIKey, sharedSecret:flickrAPISharedSecret) 28 | 29 | requests = NSMutableSet() 30 | 31 | flickrRequest = nil 32 | } 33 | 34 | // searches Flickr for the given string, returning a signal that emits the response 35 | func flickrSearchSignal(searchString: String) -> RACSignal { 36 | 37 | func photosFromDictionary (response: NSDictionary) -> FlickrSearchResults { 38 | let photoArray = response.valueForKeyPath("photos.photo") as! [[String: String]] 39 | let photos = photoArray.map { 40 | (photoDict) -> FlickrPhoto in 41 | let url = self.flickrContext.photoSourceURLFromDictionary(photoDict, size: OFFlickrSmallSize) 42 | return FlickrPhoto(title: photoDict["title"]!, url: url, identifier: photoDict["id"]!) 43 | } 44 | let total = response.valueForKeyPath("photos.total")!.integerValue 45 | return FlickrSearchResults(searchString: searchString, totalResults: total, photos: photos) 46 | } 47 | 48 | return signalFromAPIMethod("flickr.photos.search", 49 | arguments: ["text" : searchString, "sort": "interestingness-desc"], 50 | transform: photosFromDictionary); 51 | } 52 | 53 | // searches Flickr for the given photo metadata, returning a signal that emits the response 54 | func flickrImageMetadata(photoId: String) -> RACSignal { 55 | 56 | let favouritesSignal = signalFromAPIMethod("flickr.photos.getFavorites", 57 | arguments: ["photo_id": photoId]) { 58 | // String is not AnyObject? 59 | (response: NSDictionary) -> NSString in 60 | return response.valueForKeyPath("photo.total") as! NSString 61 | } 62 | 63 | let commentsSignal = signalFromAPIMethod("flickr.photos.getInfo", 64 | arguments: ["photo_id": photoId]) { 65 | (response: NSDictionary) -> NSString in 66 | return response.valueForKeyPath("photo.comments._text") as! NSString 67 | } 68 | 69 | return RACSignalEx.combineLatestAs([favouritesSignal, commentsSignal]) { 70 | (favourites:NSString, comments:NSString) -> FlickrPhotoMetadata in 71 | return FlickrPhotoMetadata(favourites: favourites.integerValue, comments: comments.integerValue) 72 | } 73 | } 74 | 75 | //MARK: Private 76 | 77 | // a utility method that searches Flickr with the given method and arguments. The response 78 | // is transformed via the given function. 79 | private func signalFromAPIMethod(method: String, arguments: [String:String], 80 | transform: (NSDictionary) -> T) -> RACSignal { 81 | 82 | return RACSignal.createSignal({ 83 | (subscriber: RACSubscriber!) -> RACDisposable! in 84 | 85 | let flickrRequest = OFFlickrAPIRequest(APIContext: self.flickrContext); 86 | flickrRequest.delegate = self; 87 | self.requests.addObject(flickrRequest) 88 | 89 | let sucessSignal = self.rac_signalForSelector(Selector("flickrAPIRequest:didCompleteWithResponse:"), 90 | fromProtocol: OFFlickrAPIRequestDelegate.self) 91 | 92 | sucessSignal 93 | // filter to only include responses from this request 94 | .filterAs { (tuple: RACTuple) -> Bool in tuple.first as! NSObject == flickrRequest } 95 | // extract the second tuple argument, which is the response dictionary 96 | .mapAs { (tuple: RACTuple) -> AnyObject in tuple.second } 97 | // transform with the given function 98 | .mapAs(transform) 99 | // subscribe, sending the results to the outer signal 100 | .subscribeNext { 101 | (next: AnyObject!) -> () in 102 | subscriber.sendNext(next) 103 | subscriber.sendCompleted() 104 | } 105 | 106 | 107 | let failSignal = self.rac_signalForSelector(Selector("flickrAPIRequest:didFailWithError:"), 108 | fromProtocol: OFFlickrAPIRequestDelegate.self) 109 | 110 | failSignal.mapAs { (tuple: RACTuple) -> AnyObject in tuple.second } 111 | .subscribeNextAs { 112 | (error: NSError) -> () in 113 | print("error: \(error)") 114 | subscriber.sendError(error) 115 | } 116 | 117 | flickrRequest.callAPIMethodWithGET(method, arguments: arguments) 118 | 119 | return RACDisposable(block: { 120 | self.requests.removeObject(flickrRequest) 121 | }) 122 | }) 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Model/FlickrSearchResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrSearchResult.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 14/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Represents the result of a Flickr search 12 | class FlickrSearchResults { 13 | let searchString: String 14 | let totalResults: Int 15 | let photos: [FlickrPhoto] 16 | 17 | init(searchString: String, totalResults: Int, photos: [FlickrPhoto]) { 18 | self.searchString = searchString; 19 | self.totalResults = totalResults 20 | self.photos = photos 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/PreviousSearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviousSearchViewModel.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 16/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Represents a search that was executed previously 12 | class PreviousSearchViewModel: NSObject { 13 | 14 | let searchString: String 15 | let totalResults: Int 16 | let thumbnail: NSURL 17 | 18 | init(searchString: String, totalResults: Int, thumbnail: NSURL) { 19 | self.searchString = searchString 20 | self.totalResults = totalResults 21 | self.thumbnail = thumbnail 22 | } 23 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/SearchResultsItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsItemViewModel.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 18/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // A ViewModel that backs an individual photo in a search result view. 12 | class SearchResultsItemViewModel: NSObject { 13 | 14 | dynamic var isVisible: Bool 15 | dynamic var favourites: NSNumber 16 | dynamic var comments: NSNumber 17 | let title: String 18 | let url: NSURL 19 | var identifier: String 20 | 21 | private let services: ViewModelServices 22 | 23 | init(photo: FlickrPhoto, services: ViewModelServices) { 24 | self.services = services 25 | title = photo.title 26 | url = photo.url 27 | identifier = photo.identifier 28 | isVisible = false 29 | favourites = -1 30 | comments = -1 31 | 32 | super.init() 33 | 34 | // a signal that emits events when visibility changes 35 | let visibleStateChanged = RACObserve(self, keyPath: "isVisible").skip(1) 36 | 37 | // filtered into visible and hidden signals 38 | let visibleSignal = visibleStateChanged.filter { $0.boolValue } 39 | let hiddenSignal = visibleStateChanged.filter { !$0.boolValue } 40 | 41 | // a signal that emits when an item has been visible for 1 second 42 | let fetchMetadata = visibleSignal.delay(1).takeUntil(hiddenSignal) 43 | 44 | fetchMetadata.subscribeNext { 45 | (next: AnyObject!) -> () in 46 | self.services.flickrSearchService.flickrImageMetadata(self.identifier).subscribeNextAs { 47 | (metadata: FlickrPhotoMetadata) -> () in 48 | self.favourites = metadata.favourites 49 | self.comments = metadata.comments 50 | } 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Util/RAC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RAC.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | // a struct that replaces the RAC macro 13 | struct RAC { 14 | var target : NSObject! 15 | var keyPath : String! 16 | var nilValue : AnyObject! 17 | 18 | init(_ target: NSObject!, _ keyPath: String, nilValue: AnyObject? = nil) { 19 | self.target = target 20 | self.keyPath = keyPath 21 | self.nilValue = nilValue 22 | } 23 | 24 | 25 | func assignSignal(signal : RACSignal) { 26 | signal.setKeyPath(self.keyPath, onObject: self.target, nilValue: self.nilValue) 27 | } 28 | } 29 | 30 | infix operator ~> {} 31 | func ~> (signal: RACSignal, rac: RAC) { 32 | rac.assignSignal(signal) 33 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Util/RACObserve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RACObserve.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | // replaces the RACObserve macro 13 | func RACObserve(target: NSObject!, keyPath: String) -> RACSignal { 14 | return target.rac_valuesForKeyPath(keyPath, observer: target) 15 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Util/RACSignal+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RACSignal+Extensions.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | // a collection of extension methods that allows for strongly typed closures 13 | extension RACSignal { 14 | 15 | func subscribeNextAs(nextClosure:(T) -> ()) -> () { 16 | self.subscribeNext { 17 | (next: AnyObject!) -> () in 18 | let nextAsT = next! as! T 19 | nextClosure(nextAsT) 20 | } 21 | } 22 | 23 | func mapAs(mapClosure:(T) -> U) -> RACSignal { 24 | return self.map { 25 | (next: AnyObject!) -> AnyObject! in 26 | let nextAsT = next as! T 27 | return mapClosure(nextAsT) 28 | } 29 | } 30 | 31 | func filterAs(filterClosure:(T) -> Bool) -> RACSignal { 32 | return self.filter { 33 | (next: AnyObject!) -> Bool in 34 | let nextAsT = next as! T 35 | return filterClosure(nextAsT) 36 | } 37 | } 38 | 39 | func doNextAs(nextClosure:(T) -> ()) -> RACSignal { 40 | return self.doNext { 41 | (next: AnyObject!) -> () in 42 | let nextAsT = next as! T 43 | nextClosure(nextAsT) 44 | } 45 | } 46 | } 47 | 48 | class RACSignalEx { 49 | class func combineLatestAs(signals:[RACSignal], reduce:(T,U) -> R) -> RACSignal { 50 | return RACSignal.combineLatest(signals).mapAs { 51 | (tuple: RACTuple) -> R in 52 | return reduce(tuple.first as! T, tuple.second as! U) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/Util/TableViewBindingHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewBindingHelper.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | @objc protocol ReactiveView { 13 | func bindViewModel(viewModel: AnyObject) 14 | } 15 | 16 | // a helper that makes it easier to bind to UITableView instances 17 | // see: http://www.scottlogic.com/blog/2014/05/11/reactivecocoa-tableview-binding.html 18 | class TableViewBindingHelper: NSObject, UITableViewDataSource, UITableViewDelegate { 19 | 20 | //MARK: Properties 21 | 22 | var delegate: UITableViewDelegate? 23 | 24 | private let tableView: UITableView 25 | private let templateCell: UITableViewCell 26 | private let selectionCommand: RACCommand? 27 | private var data: [AnyObject] 28 | 29 | //MARK: Public API 30 | 31 | init(tableView: UITableView, sourceSignal: RACSignal, nibName: String, selectionCommand: RACCommand? = nil) { 32 | self.tableView = tableView 33 | self.data = [] 34 | self.selectionCommand = selectionCommand 35 | 36 | let nib = UINib(nibName: nibName, bundle: nil) 37 | 38 | // create an instance of the template cell and register with the table view 39 | templateCell = nib.instantiateWithOwner(nil, options: nil)[0] as! UITableViewCell 40 | tableView.registerNib(nib, forCellReuseIdentifier: templateCell.reuseIdentifier!) 41 | 42 | super.init() 43 | 44 | sourceSignal.subscribeNext { 45 | (d:AnyObject!) -> () in 46 | self.data = d as! [AnyObject] 47 | self.tableView.reloadData() 48 | } 49 | 50 | tableView.dataSource = self 51 | tableView.delegate = self 52 | } 53 | 54 | //MARK: Private 55 | 56 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 57 | return data.count 58 | } 59 | 60 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 61 | let item: AnyObject = data[indexPath.row] 62 | let cell = tableView.dequeueReusableCellWithIdentifier(templateCell.reuseIdentifier!) 63 | if let reactiveView = cell as? ReactiveView { 64 | reactiveView.bindViewModel(item) 65 | } 66 | return cell! 67 | } 68 | 69 | func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat { 70 | return templateCell.frame.size.height 71 | } 72 | 73 | func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) { 74 | if selectionCommand != nil { 75 | selectionCommand?.execute(data[indexPath.row]) 76 | } 77 | } 78 | 79 | func scrollViewDidScroll(scrollView: UIScrollView!) { 80 | if self.delegate?.respondsToSelector(Selector("scrollViewDidScroll:")) == true { 81 | self.delegate?.scrollViewDidScroll?(scrollView); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/FlickrSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrSearchViewController.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | class FlickrSearchViewController: UIViewController { 13 | 14 | @IBOutlet var searchTextField: UITextField! 15 | @IBOutlet var searchButton: UIButton! 16 | @IBOutlet var searchHistoryTable: UITableView! 17 | @IBOutlet var loadingIndicator: UIActivityIndicatorView! 18 | 19 | private let viewModel: FlickrSearchViewModel 20 | private var bindingHelper: TableViewBindingHelper! 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("NSCoding not supported") 24 | } 25 | 26 | init(viewModel:FlickrSearchViewModel) { 27 | self.viewModel = viewModel 28 | 29 | super.init(nibName: "FlickrSearchViewController", bundle: nil) 30 | 31 | edgesForExtendedLayout = .None 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | bindViewModel() 38 | } 39 | 40 | private func bindViewModel() { 41 | title = viewModel.title 42 | 43 | searchTextField.rac_textSignal() ~> RAC(viewModel, "searchText") 44 | 45 | viewModel.executeSearch!.executing.not() ~> RAC(loadingIndicator, "hidden") 46 | 47 | viewModel.executeSearch!.executing ~> RAC(UIApplication.sharedApplication(), "networkActivityIndicatorVisible") 48 | 49 | searchButton.rac_command = viewModel.executeSearch 50 | 51 | bindingHelper = TableViewBindingHelper(tableView: searchHistoryTable, 52 | sourceSignal: RACObserve(viewModel, keyPath: "previousSearches"), nibName: "RecentSearchItemTableViewCell", 53 | selectionCommand: viewModel.previousSearchSelected) 54 | 55 | viewModel.connectionErrors.subscribeNextAs { 56 | (error: NSError) -> () in 57 | let alert = UIAlertView(title: "Connection Error", message: "There was a problem reaching Flickr", delegate: nil, cancelButtonTitle: "OK") 58 | alert.show() 59 | } 60 | 61 | func hideKeyboard(any: AnyObject!) { 62 | self.searchTextField.resignFirstResponder() 63 | } 64 | viewModel.executeSearch!.executionSignals.subscribeNext(hideKeyboard) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/FlickrSearchViewController.xib: -------------------------------------------------------------------------------- 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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/RecentSearchItemTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecentSearchItemTableViewCell.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 16/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RecentSearchItemTableViewCell: UITableViewCell, ReactiveView { 12 | 13 | 14 | @IBOutlet var thumbnailImage: UIImageView! 15 | @IBOutlet var totalResultsLabel: UILabel! 16 | @IBOutlet var recentSearchLabel: UILabel! 17 | 18 | func bindViewModel(viewModel: AnyObject) { 19 | let previousSearch = viewModel as! PreviousSearchViewModel 20 | recentSearchLabel.text = previousSearch.searchString 21 | totalResultsLabel.text = "\(previousSearch.totalResults)" 22 | 23 | let data = NSData(contentsOfURL: previousSearch.thumbnail) 24 | let image = UIImage(data: data!) 25 | thumbnailImage.image = image 26 | } 27 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/RecentSearchItemTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/SearchResultsTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsTableViewCell.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 16/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | class SearchResultsTableViewCell: UITableViewCell, ReactiveView { 13 | 14 | @IBOutlet var favouritesLabel: UILabel! 15 | @IBOutlet var commentsLabel: UILabel! 16 | @IBOutlet var favouritesIcon: UIImageView! 17 | @IBOutlet var commentsIcon: UIImageView! 18 | @IBOutlet var imageThumbnailView: UIImageView! 19 | @IBOutlet var titleLabel: UILabel! 20 | 21 | func bindViewModel(viewModel: AnyObject) { 22 | let photo = viewModel as! SearchResultsItemViewModel 23 | titleLabel.text = photo.title 24 | 25 | self.clipsToBounds = true 26 | 27 | imageThumbnailView.contentMode = .ScaleToFill 28 | 29 | signalForImage(photo.url).deliverOn(RACScheduler.mainThreadScheduler()) 30 | .takeUntil(self.rac_prepareForReuseSignal) 31 | .subscribeNextAs { 32 | (image: UIImage) -> () in 33 | self.imageThumbnailView.image = image 34 | } 35 | 36 | RACObserve(photo, keyPath: "favourites").subscribeNextAs { 37 | (faves:NSNumber) -> () in 38 | self.favouritesLabel.text = Int(faves) == -1 ? "" : "\(Int(faves))" 39 | self.favouritesIcon.hidden = Int(faves) == -1 40 | } 41 | 42 | RACObserve(photo, keyPath: "comments").subscribeNextAs { 43 | (comments:NSNumber) -> () in 44 | self.commentsLabel.text = Int(comments) == -1 ? "" : "\(comments)" 45 | self.commentsIcon.hidden = Int(comments) == -1 46 | } 47 | 48 | photo.isVisible = true 49 | self.rac_prepareForReuseSignal.subscribeNext { 50 | (next: AnyObject!) -> () in 51 | self.imageThumbnailView.image = nil 52 | photo.isVisible = false 53 | } 54 | } 55 | 56 | func signalForImage(imageUrl: NSURL) -> RACSignal{ 57 | let scheduler = RACScheduler(priority: RACSchedulerPriorityBackground) 58 | let signal = RACSignal.createSignal({ 59 | (subscriber: RACSubscriber!) -> RACDisposable! in 60 | let data = NSData(contentsOfURL: imageUrl) 61 | let image = UIImage(data: data!) 62 | subscriber.sendNext(image) 63 | subscriber.sendCompleted() 64 | return nil 65 | }) 66 | return signal.subscribeOn(scheduler) 67 | } 68 | 69 | func setParallax(value: CGFloat) { 70 | imageThumbnailView.transform = CGAffineTransformMakeTranslation(0, value) 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/SearchResultsTableViewCell.xib: -------------------------------------------------------------------------------- 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 | 27 | 28 | 34 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/SearchResultsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsViewController.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SearchResultsViewController: UIViewController, UITableViewDelegate { 12 | 13 | @IBOutlet var searchResultsTable: UITableView! 14 | 15 | private let viewModel: SearchResultsViewModel 16 | private var bindingHelper: TableViewBindingHelper! 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("NSCoding not supported") 20 | } 21 | 22 | init(viewModel:SearchResultsViewModel) { 23 | self.viewModel = viewModel 24 | 25 | super.init(nibName: "SearchResultsViewController", bundle: nil) 26 | 27 | edgesForExtendedLayout = .None 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | title = viewModel.title 34 | 35 | bindingHelper = TableViewBindingHelper(tableView: searchResultsTable, sourceSignal: RACObserve(viewModel, keyPath: "searchResults"), nibName: "SearchResultsTableViewCell") 36 | bindingHelper.delegate = self 37 | } 38 | 39 | func scrollViewDidScroll(scrollView: UIScrollView!) { 40 | let cells = searchResultsTable.visibleCells 41 | for cell in cells as! [SearchResultsTableViewCell] { 42 | let value = -40.0 + (cell.frame.origin.y - searchResultsTable.contentOffset.y) / 5.0; 43 | cell.setParallax(value) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/SearchResultsViewController.xib: -------------------------------------------------------------------------------- 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinEberhardt/ReactiveSwiftFlickrSearch/39306508d8d2facc862c18f7545600cd12afcc73/ReactiveSwiftFlickrSearch/View/comment.png -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/View/fave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinEberhardt/ReactiveSwiftFlickrSearch/39306508d8d2facc862c18f7545600cd12afcc73/ReactiveSwiftFlickrSearch/View/fave.png -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/ViewModel/FlickrSearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlickrSearchViewModel.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveCocoa 11 | 12 | // The top-level ViewModel, exposes an interface that allows you 13 | // to search Flickr via a search string It also displays the 14 | // history of recent searches 15 | class FlickrSearchViewModel: NSObject { 16 | 17 | //MARK: Properties 18 | 19 | dynamic var searchText = "" 20 | dynamic var previousSearches: [PreviousSearchViewModel] 21 | var executeSearch: RACCommand? 22 | let title = "Flickr Search" 23 | var previousSearchSelected: RACCommand! 24 | var connectionErrors: RACSignal! 25 | 26 | private let services: ViewModelServices 27 | 28 | //MARK: Public APIprintln 29 | 30 | init(services: ViewModelServices) { 31 | 32 | self.services = services 33 | previousSearches = [] 34 | 35 | super.init() 36 | 37 | let validSearchSignal = RACObserve(self, keyPath: "searchText").mapAs { 38 | (text: NSString) -> NSNumber in 39 | return text.length > 3 40 | }.distinctUntilChanged(); 41 | 42 | executeSearch = RACCommand(enabled: validSearchSignal) { 43 | (any:AnyObject!) -> RACSignal in 44 | return self.executeSearchSignal() 45 | } 46 | connectionErrors = executeSearch!.errors 47 | 48 | previousSearchSelected = RACCommand() { 49 | (any:AnyObject!) -> RACSignal in 50 | let previousSearch = any as! PreviousSearchViewModel 51 | self.searchText = previousSearch.searchString 52 | return self.executeSearchSignal() 53 | } 54 | 55 | } 56 | 57 | //MARK: Private methods 58 | 59 | private func executeSearchSignal() -> RACSignal { 60 | return services.flickrSearchService.flickrSearchSignal(searchText).doNextAs { 61 | (results: FlickrSearchResults) -> () in 62 | let viewModel = SearchResultsViewModel(services: self.services, searchResults: results) 63 | self.services.pushViewModel(viewModel) 64 | self.addToSearchHistory(results) 65 | } 66 | } 67 | 68 | private func addToSearchHistory(result: FlickrSearchResults) { 69 | let matches = previousSearches.filter { $0.searchString == self.searchText } 70 | 71 | var previousSearchesUpdated = previousSearches 72 | 73 | if matches.count > 0 { 74 | let match = matches[0] 75 | var withoutMatch = previousSearchesUpdated.filter { $0.searchString != self.searchText } 76 | withoutMatch.insert(match, atIndex: 0) 77 | previousSearchesUpdated = withoutMatch 78 | } else { 79 | let previousSearch = PreviousSearchViewModel(searchString: searchText, totalResults: result.totalResults, thumbnail: result.photos[0].url) 80 | previousSearchesUpdated.insert(previousSearch, atIndex: 0) 81 | } 82 | 83 | if (previousSearchesUpdated.count > 10) { 84 | previousSearchesUpdated.removeLast() 85 | } 86 | 87 | previousSearches = previousSearchesUpdated 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/ViewModel/SearchResultsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsViewModel.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // A ViewModel that exposes the results of a Flickr search 12 | class SearchResultsViewModel: NSObject { 13 | 14 | var searchResults: [SearchResultsItemViewModel] 15 | let title: String 16 | 17 | private let services: ViewModelServices 18 | 19 | init(services: ViewModelServices, searchResults: FlickrSearchResults) { 20 | self.services = services 21 | self.title = searchResults.searchString 22 | self.searchResults = searchResults.photos.map { SearchResultsItemViewModel(photo: $0, services: services ) } 23 | 24 | super.init() 25 | } 26 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/ViewModel/ViewModelServices.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelServices.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // provides common services to view models 12 | protocol ViewModelServices { 13 | 14 | // pushes the given ViewModel onto the stack, this causes the UI to navigate from 15 | // one view to the next 16 | func pushViewModel(viewModel:AnyObject) 17 | 18 | // provides the search API 19 | var flickrSearchService: FlickrSearch { get } 20 | 21 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearch/ViewModelServicesImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelServicesImpl.swift 3 | // ReactiveSwiftFlickrSearch 4 | // 5 | // Created by Colin Eberhardt on 15/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ViewModelServicesImpl: ViewModelServices { 12 | 13 | private let navigationController: UINavigationController 14 | let flickrSearchService: FlickrSearch 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | self.flickrSearchService = FlickrSearchImpl() 19 | } 20 | 21 | func pushViewModel(viewModel:AnyObject) { 22 | if let searchResultsViewModel = viewModel as? SearchResultsViewModel { 23 | self.navigationController.pushViewController(SearchResultsViewController(viewModel: searchResultsViewModel), animated: true) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearchTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ReactiveSwiftFlickrSearchTests/ReactiveSwiftFlickrSearchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveSwiftFlickrSearchTests.swift 3 | // ReactiveSwiftFlickrSearchTests 4 | // 5 | // Created by Colin Eberhardt on 06/07/2014. 6 | // Copyright (c) 2014 Colin Eberhardt. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ReactiveSwiftFlickrSearchTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | XCTAssert(true, "Pass") 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measureBlock() { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | --------------------------------------------------------------------------------