Swift

I made the first Swift UI-based app!

swiftui-app-first-en

I made the first Swift UI-based app!

After a long time, I thought I’d write some code for my personal development, so I made a simple Swift UI-based app.
I learned a lot, so I will leave it as a memorandum.
It’s an app that I made in about 3 days, so there is a possibility that there may be problems, so I plan to update that area when I have time in the future.
The sample app I made this time is an app called WorkHotel that allows you to search for a hotel with a corresponding vacancy on the map.

What I learned / tried

How to implement SearchBar

In the case of UIKit, there was something called UISearchBar, but SwiftUI also has a function to display SearchBar.
You can view it by using a modifier called searchable (Implements). However, it must be described in NavigationView .
The cancel button is also automatically displayed when focusing.

.searchable(text: $viewModel.searchText, placement: .navigationBarDrawer, prompt: Text("Search for place names"))

If you want to display the Placeholder, you can display it by specifying the parameter prompt.
It’s not very customizable, but it’s easy to implement, so if you’re not particular about the design, I think this is enough.

View of stars for review

Since I wanted to display the review information this time, I implemented a View that displays the stars for review that I often see as follows.

The implemented code can be found in StarReviewView.swift.
The implementation is to pass a Double, for example 4.5, and place 4 red stars, a red star with a width of 0.5 and a gray star behind it.

Common component of View for title + content

This time, on the details screen and option setting screen, the title + custom component was often displayed, so as a common component, SubTitleView.swift has been implemented.

This component is designed to pass a String for the title and aView for display under that title, where the View is passed using viewbuilder.

public struct SubTitleView<T: View>: View {

    private let title: String
    private let childView: T
    private let padding: CGFloat = 4

    public init( title: String, @ViewBuilder childView: () -> T) {
        self.title = title
        self.childView = childView()
    }

    public var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack {
                Text(title)
                    .font(.system(size: 18, weight: .semibold))
                    .foregroundColor(Color(.label))
                    .background(Color.clear)
                    .padding(.horizontal, padding)
                Spacer()
            }
            .frame(height: 40)
            .background(Color(UIColor.systemGray5))
            childView
                .background(Color(UIColor.systemBackground))
                .padding(padding)
        }
    }
}

AsyncImage to get images asynchronously

Until iOS14, if you want to get the URL image asynchronously, you had to implement or use the library by yourself, but iOS15 can use the standard View called AsyncImage. This time I tried using the AsyncImage (Implementation location)
For the argument, set the URL of the image to be displayed, and set transaction if you want to apply animation such as fade when displaying the image.
In the closure, you can get the value AsyncImagePhase, and you can individually sort out the views to be displayed during (1)image acquisition, (2)error, and (3)View shown while loading.
However, it does not support image cache, so if you want to cache downloaded images and reduce the number of communications, create your own custom or using libraries like swiftui-cached-async-image.

AsyncImage(url: hotelInfo.hotelImageURL, transaction: Transaction(animation: .easeInOut(duration: 0.6))) { phase in
    if let image = phase.image {
        image
        .resizable()
        .aspectRatio(contentMode: .fill)
    } else if phase.error != nil {
        Text("No image")
    } else {
        ProgressView()
    }
}

Card-shaped carousel View

In this sample app, the hotel information is displayed as a pin and a list, but the list is displayed in the form of a carousel at the bottom of the screen, and you can swipe sideways to display the previous and next cards.
The implementation is described in CarouselView.swift).

How to transition from List to Detail

Generally, if you want to make a push type screen transition in SwiftUI, you should use NavigationLink as follows.

List {
    ForEach(items, id: .self) { item in
        NavigationLink(
            destination: Text(item),
            label: {
                Text(item)
            }
        )
    }
}

However, in the case of this app, when I surrounded the view of the card with NavigationLink with the above implementation, the swipe gesture and theNavigationLink gesture were mixed, and the app did not work well.
Therefore, in this app, instead of directly enclosing the view of the card with NavigationLink as described above, tap the card to switch the flag of isPushNavigationActive and display the details screen when it becomes true.(Implements)

NavigationLink(isActive: $viewModel.isPushNavigationActive) {
    workHotelDetailView
} label: {
    EmptyView()
}

Also, for the detailed screen of the transition destination, since it is necessary to pass the detailed information fetched asynchronously with the API, it is generated at the time of screen transition with ViewBuilder.
In this View, the transition source View is a card, so in the above NavigationLink, the transition source View is EmptyView.

@ViewBuilder
private var workHotelDetailView: some View {
    if !viewModel.hotelList.isEmpty, viewModel.hotelList.count > viewModel.selectedIndex {
        WorkHotelDetailView(hotelInfo: viewModel.hotelList[viewModel.selectedIndex])
            .navigationBarTitle("Hotel Detail", displayMode: .inline)
    } else  {
        EmptyView()
    }
}

Loading page

For Loading during communication, we are using the overlay modifier.
It monitors the properties of isLoading and displays ProgressView when it is true.

.overlay {
    if viewModel.isLoading {
        ProgressView("Fetching data, please wait...")
            .progressViewStyle(CircularProgressViewStyle(tint: Color(WorkHotelCommon.themeColor)))
    }
}

Communication processing of Alamofire that supports Swift Concurrency

The communication process of this sample app uses Swift Concurrency, and Alamofire already supports SwiftConcurrency, so this time it using.
The implementation is simple, just write the following.
R is a struct of Decodable.

let response = await AF.request(requestURL, method: method).serializingDecodable(R.self).response

As a response, DataResponse <Decodable, AFError> is returned.
You can get the Decodable value with response.value, the error with response.error, and the HTTPURLResponse with response.response.
The Decode work can also be done by Alamofire, so the amount of code description is small.

Bonus: How to use Rakuten Travel Vacancy Search API

This app uses Rakuten Travel Vacancy Search API to fetch data.
As for the usage method, you can use it just by registering with RakutenID, so the hurdle of usage itself is low.
However, when I implemented it, I noticed that each property of the response does not know which one is returned as null, so if the one that is not set to Optional in Decodable is returned as null, I get a parse error with Decodable.
In order to prevent that, I think that there is no choice but to set all the property as Optional, or to use the response of API and set the one returned by null to Optional as appropriate.

Impressions!

In the image so far, SwiftUI seemed to be for simple screens and UI, but this time I implemented it based on SwiftUI, and I felt that I could make a fairly complicated UI with SwiftUI.
Various updates related to SwfitUI were announced in WWDC22 yesterday, and I felt that it would be easier to write and use apps with SwiftUI in the future. Currently, there are situations where we need to use the power of UIKit, but even considering development efficiency, I think that there will be more situations where SwiftUI will be used, so I thought I would strengthen the catch-up of SwiftUI.

+1

COMMENT

Your email address will not be published. Required fields are marked *

CAPTCHA