Building Better Views (Part II), Next Steps

Dec 26, 2019
4 minute read

If you haven't checked out Part I, I recommend reading it because if you don't, none of writing below will make sense!

Three Unanswered Questions

1. What happens when the views you want to configure are more complex?

My recommended approach is to construct a one-time use struct, specifically for displaying in that one place. This type should only have the properties you need to render the view.

struct HomeScreenCourseProgressViewDisplay {
    let course: Course
    let enrollment: Enrollment
    let customization: SchoolCustomization
}

Creating the ViewData should look familiar. We're going to do the exact same thing we did before.

extension HomeScreenCourseProgressViewDisplay: CourseProgressViewData {

    var titleLabelText: String {
        return self.course.name
    }

    var subtitleLabelText: String {
        return self.course.author.name
    }
    
    var statusLabelText: String {
        return String.localizedStringWithFormat(NSLocalizedString("%@% complete", comment: "The percentage a course is complete"), self.enrollment.percentComplete)
    }
    
    var progress: CGFloat {
        return CGFloat(self.enrollment.percentComplete) / 100
    }
    
    var imageUrl: URL? {
        return self.course.imageUrl
    }

}

Using this ViewData object is just as simple as it was before. On our home screen, we now create the struct, and configure our custom view with it. Same as before, just leveraging how lightweight creating types in Swift is!

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let currentUser = self.userAtIndexPath(indexPath: indexPath), self.hasCoursesAtIndexPath(indexPath: indexPath) else { fatalError("Ruh roh"!) }
    
    let currentCourse = currentUser.courses[indexPath.row]
    let currentEnrollment = currentUser.enrollments[indexPath.row]
    let schoolCustomization = currentUser.school.customization
    
    let homeScreenDisplay = HomeScreenCourseProgressViewDisplay(
        course: currentCourse, 
        enrollment: currentEnrollment, 
        customization: schoolCustomization
    )
    
    cell.customView.configure(viewData: homeScreenDisplay)
    
    return cell
}

2. How does the ViewData pattern deal with user interaction?

I advise keeping user actions in the UIView realm. You can continue using the delegate pattern, closures, or wherever your preferences may lie. If you’re looking to get a little more advanced, I’d consider reading Dave DeLong’s A Better MVC series.

3. Where does logic code reside, and what happens if you have more complex transformations?

The scenarios so far have worked great. The models you received from the server looked a lot like the way you plan to display them, but that's not always the case. Sometimes you're going to need business logic, and that's ok.

This is the question I had the most trouble coming up with one answer for. I realized the reason I couldn't come up with one answer is because there isn't only one answer.

Looking back at our Comment model, we see that there is a Date object in there.

public struct Comment {
    let text: String
    let commenter: String
    let createdAt: Date
    let imageUrl: URL?
}

In our first example we simply glossed over the fact that we were translating a Date into a String, by using a simple function that already exists in a third party library.

extension Comment: CommentViewData {
    var timestamp: String {
        return self.createdAt.timeAgoSinceNow
    }
}

But now let's pretend we don't have timeAgoSinceNow available to us. Where does that transformation code live? The answer is, it's up to you!

Some people prefer to make an object to handle business logic, to make their code more testable. If it makes you happy to keep it in the ViewData file, go right ahead. If not, then don't. Who am I to tell people how to be happy?

extension Comment: CommentViewData {

    var timestamp: String {
        let dateTransformer = DateTransformer(self.createdAt)
        return dateTransformer.asString()
    }
    
    private static func transformDateToString(date: Date) -> String {
        return someMagicalWayToTransformDatesToStrings()
    }

}
    
struct DateTransformer {
    let date: Date

    func asString() -> Date {
        return someMagicalDateTransformer()
    }
}

My personal preference is to use private static functions, keeping in tune with the functional nature of this approach.

extension Comment: CommentViewData {

    var timestamp: String {
        return transformDateToString(self.createdAt)
    }

}

private extension Comment {

    static func transformDateToString(date: Date) -> String {
        return someMagicalDateTransformer()
    }

}

The important thing to note is that when it comes to business logic, you have the agency to structure your codebase however you'd like. The ViewData pattern isn't prohibitive or prescriptive, it's just there to aid you in transforming models into views.


These are the big questions I've received while using this pattern over the last few years. I'm excited to hear your thoughts, and am always receptive to feedback!

Joe Fabisevich is an indie developer creating software at Red Panda Club Inc. while writing about design, development, and building a company. Formerly an iOS developer working on societal issues @Twitter. These days I don't tweet, but I do post on Threads.

Like my writing? You can keep up with it in your favorite RSS reader, or get posts emailed in newsletter form. I promise to never spam you or send you anything other than my posts, it's just a way for you to read my writing wherever's most comfortable for you.

If you'd like to know more, wanna talk, or need some advice, feel free to sign up for office hours, I'm very friendly. 🙂