Skip to content

Conversation

@mfclarke
Copy link

@mfclarke mfclarke commented Jan 22, 2018

This PR adds swiftlint (install with brew) and fixes as much as I could without doing bigger refactoring.

Hopefully without being annoying, here's some issues I noticed along the way:

  • If data comes back from the API in a format we don't expect, the app will likely crash. The code is force unwrapping deserialised API response model vars all over the place. This is a failure of AlamofireObjectMapper where you have to make all vars optional. We moved away from this on PlayPlex to the built in swift 4 Codable protocol. This is much nicer, you can use immutable non-optionals. Your init is throwable, so if something doesn't decode from json as you'd expect an error is thrown for you to handle. You can then just filter that model out etc.
  • Some force unwraps I couldn't clean up because I don't know what default to return for an Rx Observable. In ReactiveSwift you can for example return an empty SignalProducer (cold signal) which completes immediately without sending values.
  • There's captured self in a lot of places, and this will result in a retain cycles (memory leaks). I didn't have time to clean these up. See below though.

And a few notes, from a swift guy to an android guy 😄 :

  • Anytime you see a ! (force unwrap) or an as! (force cast) that's a crash waiting to happen. Always favour non-optionals where possible, or ? to safely unwrap. There's a bunch of operators to help you here:
  • Some ! are legit. Ie where you know there will always be a value but it's simpler to do it this way than fight the compiler. Or when using storyboards with @IBOutlet etc. Also where you actually do want the app to crash if something is nil or wrong type.
  • Watch out for captured self. Everytime you reference self in an anonymous closure it captures. You need to weakify self to break this. See ARC (about 2/3 way down)
  • Swift is really opinionated when it comes to naming and casing. See Swift API Design Guidelines. So the conventions are pretty different between swift and java (ie with enums).

"${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework",
"${BUILT_PRODUCTS_DIR}/RxBlocking/RxBlocking.framework",
"${BUILT_PRODUCTS_DIR}/RxTest/RxTest.framework",
"${BUILT_PRODUCTS_DIR}/SwiftHamcrest/Hamcrest.framework",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's conventional to commit the result of pod install even if you don't commit the Pods folder

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line length and general boilercrap cleanup

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 - on the android side I knew the analysis too.ls to enforce this. Not the case on iOS swift...yet.

public enum AppError : Error {
case RuntimeError(String)
public enum AppError: Error {
case runtimeError(String)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift naming guidelines

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

func autoconnect() -> Observable<E> {
return Observable.create { observer in
return self.do(onSubscribe: {
self.do(onSubscribe: {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer implicit return of explicit

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bet Aziz Ansar would have loved explicit LOL!

I had no idea swift did this. However as a learning tool I'm probably going to leave it. Less WTF moments.

}

extension Observable where Element: OptionalType {
func ignoreNil() -> Observable<Element.Wrapped> {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this for easier strong type mapping to subclasses during an observable flow. Optional map to class -> ignore nils.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice...not 100% sure what it does but the less manual casting the better

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, RxSwiftExt (a collection of RxSwift extensions from the RxSwift contributors) has an option similar to this already called .unwrap()


return Observable.just(NowPlayingInfoImpl.init(movieInfoList: movieInfoList,
pageNumber: serviceResonse.page!,
totalPageNumber: serviceResonse.total_pages!))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift naming guidelines

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

case FAILURE
case inFlight
case success
case failure
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift naming guidelines

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to find where they talk about ENUMS. From ADA To C to Java...always been caps. Easier for readability that the code speaks about enums vs other types.

Just trusting and verifying :-)

var popularity: Double!
var voteCount: Int!
var video: Bool!
var voteAverage: Double!
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force unwraps here rather than scattered throughout the code. The real solution to this is to convert to Codable but had no time

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might try that on another PR. For now this is good for educational tool

vote_count <- map["vote_count"]
voteCount <- map["vote_count"]
video <- map["video"]
vote_average <- map["vote_average"]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift naming guidelines

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😲 well look at that.

Add passed in list to table view with animation for each entry to add
Parameters: listToAdd - list to add
*/
func addAll(listToAdd: Array<MovieViewInfo>!) -> Void {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-> Void is implicit. This (and a lot of spacing and implicit return stuff) was fixed with 1 pass of swiftlint autocorrect 💯

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the beginning I didn't have the void on my functions, then started putting it in. No idea why..

👍

if (movieViewInfo is MovieViewInfoImpl) {
if movieViewInfo is MovieViewInfoImpl {
// swiftlint:disable:next force_cast
let movieCell: MovieCell = tableView.dequeueReusableCell(withIdentifier: "MovieCellIdentifier", for: indexPath) as! MovieCell
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't mind force casting in this case. A crash here is basically the only sane thing to do as you won't know how the app will behave with a cell of the wrong type

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

nowPlayingViewModel?.processUiEvent(uiEvent: scrollEvent)
} else {
if (uiModel.adapterCommandType == AdapterCommandType.ADD_DATA) {
if uiModel.adapterCommandType == .addData {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't clean this up everywhere but I generally prefer removing the prefix where it's implicit

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if I start typing "." on Xcode it'll expand all the enum types? I guess I'll find out...
👍

assertThat(movieInfo.getPictureUrl(), matchesPattern(IMAGE_PATH + "/tnmL0g604PDRJwGJ5fsUSYKFo9.jpg", options: .caseInsensitive))
assertThat(movieInfo.getRating(), equalTo(7.2))
// TODO: This fails outside of US timezones
assertThat(movieInfo.getReleaseDate().description, equalTo("2017-03-15 04:00:00 +0000"))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you can set an override timezone to NSCalendar or soemthing but didn't have a chance to do it

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 leaving in TODO


let fileLogger: DDFileLogger = DDFileLogger() // File Logger
fileLogger.rollingFrequency = TimeInterval(60*60*24) // 24 hours
fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

//App Singleton
//
container.register(.singleton) { ServiceApiImpl(baseUrl: "https://api.themoviedb.org/3/movie") as ServiceApi}
container.register(.singleton) { ServiceApiImpl(baseUrl: "https://api.themoviedb.org/3/movie") as ServiceApi }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// VC Singleton
//
container.register {serviceController in NowPlayingViewModel(serviceController: serviceController) as NowPlayingViewModel}
container.register { serviceController in NowPlayingViewModel(serviceController: serviceController) as NowPlayingViewModel }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


/// Init an error for a local client-side error. Usually this would be the NSError reported by NSURLSession, e.g. network connection not available.
/// Init an error for a local client-side error. Usually this would be the NSError reported by NSURLSession,
/// e.g. network connection not available.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


func indexOf(element: Element) -> Int {
guard let index = index(where: { $0 as AnyObject === element as AnyObject}) else { return -1 }
guard let index = index(where: { $0 as AnyObject === element as AnyObject }) else { return -1 }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.flatMap{ (serviceResponse: ServiceResponse) -> Observable<NowPlayingInfo> in
return TranslateNowPlayingSubscriptionFunc.init(imageUrlPath: self.imageUrlPath).apply(serviceResonse: serviceResponse)
.flatMap { (serviceResponse: ServiceResponse) -> Observable<NowPlayingInfo> in
TranslateNowPlayingSubscriptionFunc.init(imageUrlPath: self.imageUrlPath).apply(serviceResonse: serviceResponse)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll probably add the return back in for learning. put some kind of note in the readme...why they are there lol

import RxSwift

protocol NowPlayingInteractor : class {
protocol NowPlayingInteractor: class {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.catchError{ (error: Error) -> Observable<ScrollResult> in
return Observable.just(ScrollResult.failure(pageNumber: scrollAction.getPageNumber(), error: error))
.catchError { (error: Error) -> Observable<ScrollResult> in
Observable.just(ScrollResult.failure(pageNumber: scrollAction.getPageNumber(), error: error))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here...

.flatMap { action -> Observable<ScrollAction> in
DDLogInfo("Thread name: " + Thread.current.description + " Translate Actions into ScrollActions.")
return Observable.just(action as! ScrollAction);
return Observable.just(action)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have the extension and just do this.

*/
init(pictureUrl: String, title: String, releaseDate: Date, rating: Double) {
self.pictureUrl = pictureUrl;
self.pictureUrl = pictureUrl
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

excluded: # paths to ignore during linting. overridden by `included`.
- Pods
- .git
- ReactiveArchitectureTests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on the components side, we allow swiftlint to lint our test code, but with custom rule sets.
Test code is still code that needs to follow standards

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're strict lol. Good to know though. I 'll leave for now.


return movieCell
} else {
// swiftlint:disable:next force_cast
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

//
if (scrollDisposable != nil) {
return;
guard self.scrollDisposable == nil else {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loving this is a keyword
👍

return (self.nowPlayingInteractor?.processAction(actions: actions))!
})
.scan(initialUiModel) { (uiModel: UiModel!, result: Result!) in
.map { $0 as? ScrollResult }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this I'll have to reject. Although it's interesting I'll explain why.

As far as the nil...I know RxJava 2 I can't push nulls. for RxSwift I'll leave this in with a comment.

There will be a case where we have more than one type of Result. You'll need to switch off the class type just like you do in drawing the cell view. I'll pull this and make the changes.

@HIFILEO
Copy link
Owner

HIFILEO commented Feb 6, 2018

Max - thanks for the PR. I took most of your suggestions and incorporated them in via another branch. I took 80% of the suggestions. The rest I just marked with notes so people using this example understand the underlying weaknesses. Please close this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants