[02] MyDay

洪旻昌
海大 SwiftUI iOS / Flutter App 程式設計
9 min readOct 25, 2023
MyDay Logo

MyDay is an application that shows school related information, details on students’ enrolled courses, as well as personal academic information.

I. Demo Video

MyDay Demo Video

II. GitHub

III. Application Icon

MyDay Application Icon

IV. Application Launch Screen (with Animation & Transition)

Notes: includes Information Property List, Custom Font
Additional feature: ".navigationBarTitleDisplayMode(.inline)"
Application Launch

Information Property List

Launch Screen → Background color

Information Property List

Home Page

a. Uses scale transition when image enters the screen with easeInOut animation when screen appears (onAppear).

b. The “welcome” text uses a custom font with bouncy animation, which causes its blinking effect, and clicking it would redirect to the main page.

NavigationStack{ //redirect to another view
VStack(spacing: 30.0){ //controls image
if show{
Image(.displayLogo)
.resizable()
.scaledToFit()
.transition(.scale)
}
}
.animation(.easeInOut(duration: 3), value: show)
.onAppear { //occur when entering view
show = true
}

NavigationLink{ //controls next view
MultipleTabView()
}label:{ //sets label which is clickable by the user
Text("welcome")
.foregroundStyle(.blue) //font color
.font(.custom( //custom font
"Bip Hope", fixedSize: 30
))
.animation(.bouncy(duration: 0.5).repeatForever(autoreverses:true).delay(3), value: show) //blinking effect
}
.navigationTitle("Home Page") //current view's title
.navigationBarTitleDisplayMode(.inline) //title's location on view
}

V. Custom Font (Additional Component)

Steps:

1. Download font with extension .otf or .ttf (there are plenty online).

2. Create a folder and insert font into the folder.

Font Folder

3. Add a new key “Fonts provided by application” in Info.plist and type in the custom fonts.

Font Setup

4. It could be applied on Text() with the following syntax:

Text("...")
.font(.custom("fontName", fixedSize: 10))

//Application

Text("welcome")
.foregroundStyle(.blue) //font color
.font(.custom("Bip Hope", fixedSize: 30))

//font size adjustment
Text("...")
.font(.system(size: 15))

VI. TabView

Additional feature: the use of enum and .tag(enum.xx)

1. There are three different pages: Home, Courses, Profile.

a. Home displays introduction and information upon the school.

b. Courses displays the students’ current enrolled courses.

c. Profile displays the students’ academic information.

2. NavigationStack and TabView

a. NavigationStack is used in switching views.

b. TabView is used in switching pages.

— Pair a page with a set of icon and text to differentiate pages.

— Gives a tag on each page to set its NavigationTitle.

— Set AccentColor in Assets to set its background color (dark&light mode).

MyDay Tabs
enum Tabs: String{ //creates enum for each page
case Home
case Courses
case Profile
}
struct MultipleTabView: View {
@State var selectionTab: Tabs = .Home //set default enum

var body: some View {
NavigationStack{ //stack for navigation
TabView(selection: $selectionTab) { //redirecting between pages
HomePageView() //destination
.tabItem{
Label("Home", systemImage: "house") //clickable component
}
.tag(Tabs.Home) //page tag
ToDoList()//destination
.tabItem{
Label("Courses", systemImage: "text.book.closed.fill")//clickable component
}
.tag(Tabs.Courses)//page tag
ProfileView()//destination
.tabItem{
Label("Profile", systemImage: "person.crop.circle.fill")//clickable component
}
.tag(Tabs.Profile)//page tag
}
.navigationTitle(selectionTab.rawValue.capitalized)//page title
}
}
}

VII. ScrollView + LazyVGrid (+ LazyVStack, + LazyHStack)

Notes: includes LazyVStack (with Section), LazyHStack, clipShape()
Additional feature: ".multilineTextAlignment(.center)"
Home View

There are two types of ScrollView: horizontal and vertical

ScrollView(.vertical){ //scroll direction: vertical
/*content*/
ScrollView(.horizontal){ //scroll direction: horizontal
/*content*/
}
/*content*/
}

There are two types of LazyGrid: LazyVGrid and LazyHGrid

LazyVGrid(columns: [GridItem()){ //sets vertical grid
/*content: loops an array*/
}

LazyHGrid(rows: [GridItem()]{ //sets horizontal grid
/*content: loops an array*/
}

**There are two types of LazyStack: LazyVStack and LazyHStack

LazyVStack{ //sets vertical stack
/*content*/
LazyHStack{ //sets horizontal stack
/*content*/
}
/*content*/
}

/*content could contain "Section(header: Text(" "), content: { ... })"

Application

ScrollView(.vertical){ //scroll vertically
ScrollView(.horizontal){ //scroll horizontally
LazyHStack{ //sets horizontal stack
NavigationLink{...} //switch between pages

Link(destination: ..., label: {...}) //redirect to a URL link

Link(destination: ..., label: {...}) //redirect to a URL link
}
}
LazyVStack(...){ //sets vertical stack
LazyHStack(...){ //sets horizontal stack
Section(header: //divides into sections with a specified header
Text("卓越教學與特色研究兼具\n的海洋國際頂尖大學")
.font(.custom("SourceHanSerifTC-Bold", fixedSize: 25))
.padding(.horizontal, 30.0)
.multilineTextAlignment(.center) //sets multiline alignment orientation: middle
.background(.quaternary)
.clipShape(RoundedRectangle(cornerRadius: 20)), //clips based on a rounded rectangle shape
content: { //content of stack
LazyVGrid(columns: [GridItem()], content: { //sets a vertical grid
ForEach(pageContent, id : \.self) { par in //loops through an array
Text("\t" + par + "\n")
.font(.system(size: 15))
}
})
.padding(.top, -20.0)
.padding()
})
}
.frame(width:380, alignment: .center)
}
}
.onAppear(){...} //action when this page appears
}

VIII. NavigationStack and NavigationLink

NavigationStack is used when an application requires to switch between pages which is helped by NavigationLink that connects the two pages.

Courses View
NavigationStack{
...
NavigationLink{
destination: /*DestinationView()*/, label:{
...
}
}
...
}

//Application

NavigationStack{ //creates stack
List{ //creates a list of objects
ForEach(courseTaken) {courses in //loops through an array
NavigationLink( //creates a link between current page and destination page
destination:ToDoDetailView(courseInfo:courses), //sets destination page
label:{ToDoRow(courseInfo:courses) //sets label of link
})
}
}
}

IX. Video Player (+ Music Player)

Note: contains Music Player

Video Player

Video View
  1. import AVKit
  2. Use a state variable to store AVPlayer(), which is a controller object of a media asset.
  3. Use VideoPlayer(player: player) to view the display content of a player.
  4. When entering page, set the state variable to a media file from a url .
import SwiftUI
import AVKit

struct VideoView: View {
@State var videoPlayer = AVPlayer()
var body: some View {
VStack{
VideoPlayer(player: videoPlayer)
.onAppear() {
videoPlayer = AVPlayer(url: Bundle.main.url(forResource: "video70YearsNTOU", withExtension: "mp4")!) //files exist in data storage
}
.frame(height:300)
Text("國立臺灣海洋大學70週年校慶")
}
}
}

Music Player

Note: result can be seen in the demo video.
  1. import AVKit
  2. Use a state variable to store AVPlayer(), which is a controller object of a media asset.
  3. Use a state variable to store boolean: the music is playing or not.
  4. Use VideoPlayer(player: player) to view the display content of a player.
  5. When entering page, set the state variable to a media file from a url and then replace the current item in the variable with the new media.
  6. Using “xx.play()” to play the music and “xx.pause()” to pause the music.
import SwiftUI
import AVKit


struct VideoView: View {
@State var musicPlayer = AVPlayer()
@State var isPlaying: Bool = false
var body: some View {
VStack{
...
//musicPlayer.pause() / musicPlayer.play()
}
.onAppear(){
if(!isPlaying){
let fileURL = Bundle.main.url(forResource:"backgroundMusic", withExtension: "mp3")!
let player = AVPlayerItem(url: fileURL)
musicPlayer.replaceCurrentItem(with: player)
}
}
}
}

X. Link

Link is used for navigating to a URL.

Link to https://www.ntou.edu.tw
Link(destination: URL(string: "...")!, label:{...})

//Application

Link(destination: URL(string: "https://www.ntou.edu.tw")!, label: {
IconView(iconLogo: "logoNTOU", iconText: "學校網站")
})

XI. Extract Subview

Extracting a section of a program that is repetitive into a subview. This can be done by blocking the section of a certain program, right click on mouse and choose the option “Extract Subview”.

//subview called by the above program section
struct IconView: View {
let iconLogo:String
let iconText:String
var body: some View {
VStack(spacing: -10.0){
ZStack{
Circle()
.fill(.white)
.stroke(.black,lineWidth: 2)
.frame(width:150)
Image(iconLogo)
.resizable()
.scaledToFit()
.frame(height:100)
.frame(width:150,height:150)
.background(.clear)
.clipShape(Circle())
}
.padding()

Text(iconText)
.foregroundStyle(.blue)
.bold()
}
.padding(.bottom)
}
}

XII. List

List is a container that displays data as row(s) in a single column.

List in Courses View
List{
ForEach(/*array*/){/*string as variable*/ in
...
}
}

// Application

List{
ForEach(courseTaken) {courses in
NavigationLink(
destination:ToDoDetailView(courseInfo:courses),label:{
ToDoRow(courseInfo:courses)
})
}
}

XIII. SF Symbols

SF Symbols is a collection of symbols that can be used for free in designing a view. Not only that, these symbols are also configurable.

SF Symbols
Image(systemName: "name of symbol")

// Application

Image(systemName: "person.text.rectangle.fill")
.resizable()
.scaledToFit()
.frame(width:25, height:25)
.frame(width:40, height:40)
.background(.yellow)
.clipShape(Circle())

XIV. Dark Mode

There are two kinds of appearance modes: Light Mode and Dark Mode. Therefore, when designing an application, it is required to consider the display in both of the appearance modes.

Light Mode and Dark Mode

XV. Date (Additional Feature)

Date with format "dd MMMM YYYY HH:mm"

Set Date

//time interval since 00:00:00 UTC on 1 January 2001
user.studentBirthday = Date(timeIntervalSinceReferenceDate: 63734400.0)

Converting Date to String

func dateToString(date : Date) -> String{
let dateFormatter = DateFormatter()
//date time string formatting
dateFormatter.dateFormat = "dd MMMM YYYY HH:mm"
return dateFormatter.string(from: date)
}

Calculating Age using Date

//calculate year differences to decide age
Calendar.current.dateComponents([.year], from: user.studentBirthday, to: Date.now).year!

Weekday: Sunday ~ Saturday

//looks for what weekday is today
Calendar.current.component(.weekday, from: Date.now)

XVI. Gradient Background (Additional Feature)

Image with Gradient Background
LinearGradient(gradient: Gradient(colors: [/*array of colors*/], startPoint: /*.orientation*/, endPoint: /*.orientation*/)

//Application


Image(displayImage)
.resizable()
.scaledToFill()
.transition(.opacity.animation(.bouncy))
.frame(width:200, height:200)
.frame(width:350)
.clipShape(Circle())
.background(LinearGradient(gradient: Gradient(colors: [.green, .blue]), startPoint: .leading, endPoint: .trailing)
.clipShape(Circle())
.frame(width: 215,height:215))

XVII. Multiple Style Text

Bold Text + Normal Text
Text("...")/*.modifier*/ + Text("...")/*.modifier*/

//Application

Text("年齡:")
.bold() +
Text("\(age)")

XVIII. Struct Data

Creates a struct data type which has relevant data type element.

import Foundation

struct Tasks: Identifiable{
let id = UUID() //universally unique identifier
let courseName: String
let courseDay: Int
let courseInstructor: String
let courseCredit: Int
let courseClass: String
}

extension Tasks{
static let defaultTask = Tasks(courseName: "無", courseDay: 1, courseInstructor: "無", courseCredit: 0, courseClass: "無")
}

XIX. 心得

Designing an application page is extremely difficult and time consuming, especially if the result isn't the same as what was expected. Learning many components and functions in a short amount of time is really tiring, but it will be of use in the future.

XX. Final Result

--

--