Comparison of Flutter and SwiftUi Declarative UI Implementation — A tribute to a famous Japanese news app
I’ve compared the implementations of Flutter and SwiftUi which are declarative UI. I didn’t think a comparison of typical UI implementations would be usable in real-world situations, so I decided to implement the UI of a famous Japanese news app to make it a valuable article in practice.
First, take a look at the final version. Naturally, both Flutter and SwiftUi were able to reproduce the UI of a famous Japanese news app almost perfectly.
However, there are some things that I couldn’t do, so I’ll mention them in the article.
final version
Flutter on the left and SwiftUi on the right
Now let’s compare the implementations in order.
The Foundation of the UI
This section compares the foundation needed to make up each UI component. There is a specific way to write both of them.
First of all, let’s look at Flutter.
Flutter comes with the MaterialApp class. Most apps start by applying the necessary objects to this class, such as the theme property and the home property.The theme property is used to apply the app’s common color and font.
For the theme property, I apply the LightTheme.pattern(context) and for darkTheme, I apply DarkTheme.pattern(context). This means that Flutter can easily handle the light and dark modes of iOS.
For the home property, I apply a TopScreen object which is where all the UI starts.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Name',
theme: LightTheme.pattern(context),
darkTheme: DarkTheme.pattern(context),
home: TopScreen(),
);
}
}
Next, let’s look at SwiftUi.
SwiftUi generates the first screen the TopView object in the SceneDelegate class.
The TopView object applies to the rootViewController property of the UIWindow class.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let topView = TopView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: topView)
self.window = window
window.makeKeyAndVisible()
}
}
}
Both Flutter and SwiftUi have a fixed writing style. Unlike Flutter, SwiftUi does not allow you to specify a common color or font for the App.
The next step is to implement a tab that will take you to four screens at the bottom of the screen: Home, Tools, Announcements, and Others.
First, let’s look at the Flutter.
The Flutter provides the Scaffold class that serves as a scaffold for material design: the appBar property, the body property, and the bottomNavigationBar property. the AppBar property is the top part of your app, where the title and back button of your app are placed. the bottomNavigationBar property is where tabs are placed to go to 4 screens: Home, Tools, Information, and Others. you need to be careful about which widget you place the send button on. If you place a send button on the bottomNavigationBar property when there is an input field on the screen, the keyboards will be overlapped and you may not be able to press the send button.
You can use the BottomNavigationBar class to represent tabs, and apply a widget for tabs to the BottomNavigationBar class with a List. The class receives information about whether or not a screen has been tapped, and uses this information to handle the screen to which it is transitioning. If you apply the BottomNavigationBar class to the bottomNavigationBar property, you can achieve a bottom tab representation.
class TopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _LayoutWidget();
}
}
class _LayoutWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: HomeScreen(),
bottomNavigationBar: _bottomNavigationBar(context));
}
void _tap(BuildContext context, StatelessWidget screen) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return screen;
}));
}
AppBar _appBar() {
return AppBar(
titleSpacing: 5.0,
elevation: 0.0,
title: SearchBar(),
actions: [
IconButton(
icon: Icon(Icons.filter_none, color: Colors.grey),
onPressed: () => null),
],
backgroundColor: Colors.white,
bottom: PreferredSize(
child: Container(
color: Color(0xffC0C0C0),
height: 0.4,
),
preferredSize: Size.fromHeight(0.4)));
}
Widget _bottomNavigationBar(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.transparent, boxShadow: [
BoxShadow(
color: Colors.transparent, spreadRadius: 0, blurRadius: 0)
]),
child: SizedBox(
height: 100,
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: 30,
selectedItemColor: Colors.red,
unselectedItemColor: Colors.grey,
onTap: (int index) {
switch (index) {
case 0:
_tap(context, HomeScreen());
break;
case 1:
_tap(context, ToolScreen());
break;
case 2:
_tap(context, InformationScreen());
break;
case 3:
_tap(context, OtherScreen());
break;
}
},
items: <BottomNavigationBarItem>[
_bottomNavigationBarItem(Icons.home, 'ホーム'),
_bottomNavigationBarItem(Icons.view_module, 'ツール'),
_bottomNavigationBarItem(Icons.notifications, 'お知らせ'),
_bottomNavigationBarItem(Icons.more_horiz, 'その他'),
])));
}
BottomNavigationBarItem _bottomNavigationBarItem(
IconData icon, String label) {
return BottomNavigationBarItem(
icon: Stack(children: [
Padding(
padding: EdgeInsets.only(left: 4, right: 4),
child: Icon(icon),
),
Positioned(
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(4)),
constraints: BoxConstraints(minHeight: 8, minWidth: 8),
child: Container()))
]),
title: Text(label, style: TextStyle(fontSize: 10)),
);
}
}
the Scaffold class appBar property has the _appBar function, the body property has the HomeScreen instance, the bottomNavigationBar property has the _bottomNavigationBarItem function. Declarative UI tends to be deep in layers, so this is divided into smaller parts as much as possible to improve the visibility. the SearchBar instance is placed in the _appBar function. Padding and margin adjustment is troublesome, so if you want to simply place a widget at the top of the screen, use appBar property.
Next, let’s look at SwiftUi.
SwiftUi doesn’t have anything like Flutter’s Scaffold class. Instead, I use the TabView class to achieve a tab representation. This just applies the screen you want to transition to with a tap event. There is no need to handle screen transitions.
struct TopView: View {
var body: some View {
TabView{
HomeScreen()
.tabItem {
VStack {
Image(systemName: "house.fill")
Text("ホーム")
}
}.tag(1)
ToolScreen()
.tabItem {
VStack {
Image(systemName: "square.grid.2x2.fill")
Text("ツール")
}
}.tag(2)
InformationScreen()
.tabItem {
VStack {
Image(systemName: "bell.fill")
Text("お知らせ")
}
}.tag(3)
OtherScreen()
.tabItem {
VStack {
Image(systemName: "ellipsis")
Text("その他")
}
}.tag(4)
}
}
}
struct TopView_Previews: PreviewProvider {
static var previews: some View {
TopView()
}
}
In SwiftUi, the source is simple because the TabView class does some UI tweaking. in Flutter, the source looks more complex because of the padding and textStyle applied. Also, the TabView class in SwiftUi has a transparent background by default. The news app I homaged also had a slightly transparent background in the tab part. So in the case of SwiftUi, I was able to achieve this without thinking about it. In Flutter’s BottomNavigationBar class, on the other hand, this wasn’t possible.
SearchBar
Compare the implementation of the search box at the top of the screen.
In both Flutter and SwiftUi, I have created a SearchBar object as a class (or struct) for the search box, but they apply in different places.
in Flutter’s case, I apply it to the appBar property of the Scaffold class mentioned above. This alone is enough to put a nice look at the top of the screen, and since SwiftUi doesn’t provide a class to build the screen UI like the Scaffold class, I just apply it as a normal, single element of the screen, by placing it at the top of the VStack struct. Now, let’s take a look at the contents of the SearchBar object.
First, let’s look at the Flutter. the UI is represented as a widget in Flutter.
In the source, I create a widget as the return value of the build function. There are many widgets available, but I use the Container widget as a base. I often use Containers to determine the overall size of a widget and the background color. This time, the blue frame of the search frame is created in decoration property of Container. Moreover, the double.infinity was applied to the width property which becomes the maximum size. This makes it possible to adjust the size of the search box to match the size of the screen, even if the screen size is large.
class SearchBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 40,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Color(0xff0066FF), width: 2),
borderRadius: BorderRadius.circular(5)),
child: Row(
children: [
Expanded(flex: 1, child: Icon(Icons.search, color: Colors.grey)),
Expanded(
flex: 6,
child: Text("xxxxxxx検索",
style: TextStyle(color: Colors.grey, fontSize: 14))),
Expanded(flex: 1, child: Icon(Icons.mic, color: Colors.grey)),
Expanded(
flex: 2,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0.0))),
color: Color(0xff0066FF),
child: Text("検索",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
onPressed: () => {}))
],
),
);
}
}
Next, let’s look at SwiftUi.
We’re going to build a UI based on the HStack struct, which is the Row widget in Flutter, and since the HStack struct creates gaps between components by default, we’re going to apply 0 to the spacing parameter to prevent those gaps from forming. Personally, I feel that spacing should be the default 0.
struct SearchBar: View {
fileprivate func searchBar() -> some View {
return HStack(spacing: 0) {
HStack(spacing: 0) {
Spacer().frame(width:10)
Image(systemName:"magnifyingglass")
.resizable()
.frame(width: 15, height: 15)
.foregroundColor(.gray)
Spacer().frame(width:10)
Text("xxxxxxx検索")
.foregroundColor(Color.gray)
.font(.system(size: 12))
Spacer()
Image(systemName:"mic.fill" ).foregroundColor(.gray)
Spacer().frame(width:10)
Button(action: {}) {
Text("検索")
.foregroundColor(Color.white)
.font(.system(size: 15))
.fontWeight(.bold)
}
.padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10))
.background(Color.blue)
}
.frame(maxWidth: .infinity)
.padding(EdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1))
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(Color.blue, lineWidth: 2)
)
Spacer().frame(width: 10)
Image(systemName:"square.on.square" ).foregroundColor(.gray)
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}
var body: some View {
VStack(spacing: 0.0) {
searchBar()
}
}
}
Login and ID Acquisition
First, let’s look at the Flutter.
In the Container widget, I used the Row widget to put a login button and a new ID registration button next to each other. I used the FlatButton widget for the buttons. I’m a little confused about how to achieve the gray separator between the login button and the New ID button. For horizontal lines, I could easily achieve this with the Divider class, but this wasn’t possible for vertical lines. In conclusion, I applied height: 20.0, width: 0.4, color: Colors.gray to the Container widget to create a vertical separator.
class Login extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(width: 0.4, color: Color(0xffC0C0C0))),
),
child: Row(children: [
Expanded(
flex: 1,
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(),
color: Color(0xffF5F5F5),
child: Text("ログイン",
style: TextStyle(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.bold)),
onPressed: () => {})),
Container(height: 20.0, width: 0.4, color: Colors.grey),
Expanded(
flex: 1,
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(),
color: Color(0xffF5F5F5),
child: Text("ID新規取得",
style: TextStyle(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.bold)),
onPressed: () => {}))
]));
}
}
Next, let’s look at SwiftUi.
For the vertical line, I had a bit of trouble with in Flutter, I was able to achieve it in SwiftUi by simply writing Divider(). I thought it was useful to be able to specify the decoration.
struct Login: View {
fileprivate func login() -> some View {
return HStack {
Button(action: {}){
Text("ログイン")
.foregroundColor(Color.black)
.font(.system(size: 12))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
Button(action: {}){
Text("ID新規取得")
.foregroundColor(Color.black)
.font(.system(size: 12))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0))
.frame(height: 30.0, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color.init(hex: "eee"))
.border(Color.init(hex: "bbb"), width: 0.5)
}
var body: some View {
VStack(spacing: 0.0) {
login()
}
}
}
Tile menu : Mail, weather, fortune telling, etc.
First, let’s look at Flutter.
The children property of the Row widget are used to set the widgets of each menu item, but the widget can be generated by the common function. To use this common function, I used generate, which is the constructor of the List class, to perform the looping process in the widget tree.
class Menu1 extends StatelessWidget {
final List<Map<String, dynamic>> menuData1 = [
{"icon": Icons.mail_outline, "text": "メール"},
{"icon": Icons.wb_sunny, "text": "天気"},
{"icon": Icons.stars, "text": "占い"},
{"icon": Icons.star_border, "text": "お気に入り"},
{"icon": Icons.add_shopping_cart, "text": "ショッピング"}
];
final List<Map<String, dynamic>> menuData2 = [
{"icon": Icons.accessibility, "text": "スポーツナビ"},
{"icon": Icons.account_balance, "text": "オク"},
{"icon": Icons.train, "text": "路線情報"},
{"icon": Icons.account_balance_wallet, "text": "スロットくじ"},
{"icon": Icons.all_inclusive, "text": "すべて"}
];
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: List.generate(
menuData1.length,
(index) => Expanded(
child: _menu(
icon: menuData1[index]["icon"],
text: menuData1[index]["text"])))),
Container(
color: Color(0xffEEEEEE),
padding: EdgeInsets.only(bottom: 6.0),
child: Row(
children: List.generate(
menuData2.length,
(index) => Expanded(
child: _menu(
icon: menuData2[index]["icon"],
text: menuData2[index]["text"])))))
],
);
}
Widget _menu({IconData icon, String text}) {
return text != "すべて"
? GestureDetector(
onTap: () => {},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(
right: BorderSide(width: 0.4, color: Colors.black12),
bottom: BorderSide(width: 0.4, color: Colors.black12))),
padding: EdgeInsets.only(top: 7, bottom: 7),
child: Column(children: [
Icon(icon),
Text(text, style: TextStyle(fontSize: 10))
])))
: Container(
decoration: BoxDecoration(
color: Color(0xffEEEEEE),
border: Border(
right: BorderSide(width: 0.4, color: Colors.black12),
bottom: BorderSide(width: 0.4, color: Colors.black12))),
child: SizedBox(
height: 53.0,
child: Padding(
padding: EdgeInsets.only(top: 18.0),
child: Text("すべて",
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black54),
textAlign: TextAlign.center))));
}
}
Next, let’s look at SwiftUi.
By placing two HStack structs under the VStack struct, I represent two rows of menus.
As with Flutter, each menu is generated by the common function. I tried to invoke a common function in the looping process by using ForEach in the HStack struct, but the common function I created is generic If it is described in ForEach, a compile error occurred. I did not use ForEach. It is redundant, but I added the common function in the HStack struct.
struct Menu1: View {
@State private var isMailScreenPresented = false
@State private var isWeatherScreenPresented = false
fileprivate func menu1() -> some View {
return VStack(spacing: 0) {
Divider()
HStack(spacing: 0) {
tile(imageName: "envelope",
label: "メール",
isPresented: $isMailScreenPresented,
tapGesture: {self.isMailScreenPresented.toggle()},
screen: MailScreen(isMailScreenPresented: self.$isMailScreenPresented))
Divider()
tile(imageName: "sun.max",
label: "天気",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "star.circle",
label: "占い",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "star",
label: "お気に入り",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "cart",
label: "ショッピング",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
}
Divider()
HStack(spacing: 0) {
tile(imageName: "sportscourt",
label: "スポーツナビ",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "hammer",
label: "オク!",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "tram.fill",
label: "経路情報",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
tile(imageName: "cube.box",
label: "スロットくじ",
isPresented: $isWeatherScreenPresented,
tapGesture: {self.isWeatherScreenPresented.toggle()},
screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
Divider()
Text("すべて").font(.system(size: 12))
.frame(height: 52, alignment: .center)
.frame(maxWidth: .infinity)
.background(Color.init(hex: "eee"))
.onTapGesture {}
}
Divider()
.padding(EdgeInsets(top: 0, leading: 0, bottom: 7, trailing: 0))
.background(Color(red: 230/255, green: 230/255, blue: 230/255))
}
}
fileprivate func tile<T:View>(imageName: String,
label: String,
isPresented: Binding<Bool>,
tapGesture: @escaping () -> Void,
screen: T) -> some View {
return VStack(spacing: 0) {
Image(systemName:imageName ).foregroundColor(.gray)
.padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
Text(label).font(.system(size: 8))
}
.frame(height: 48, alignment: .center)
.frame(maxWidth: .infinity)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0))
.onTapGesture {tapGesture()}
.sheet(isPresented: isPresented) {
screen
}
}
var body: some View {
VStack(spacing: 0.0) {
menu1()
}
}
}
Scroll through the menu: StayHome, News, Coupons, Entertainment, etc.
First, let’s look at the Flutter.
I need to make it scroll horizontally, so I used the SingleChildScrollView widget and applied Axis.horizontal to the scrollDirection property. By default, the scrolling will be vertical. I used generate, which is a constructor of the List class, to create each menu item with the common functions, because I want to do the loop process in the widget tree.
I also use the GestureDetector widget to get a tap event, so when you tap a widget wrapped in the GestureDetector widget, it calls a process that is applied to the onTap property. Although I haven’t applied any processing, I will be able to switch menus and page transitions here.
class Menu2 extends StatelessWidget {
final List<String> menuData = [
"すべて",
"ニュース",
"新型コロナ",
"クーポン",
"芸能",
"スポーツ",
"話題",
"フォロー",
"東京五輪"
];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
menuData.length, (index) => _tabMenu(menuData[index]))));
}
Widget _tabMenu(String label) {
return GestureDetector(
onTap: () => {},
child: Container(
color: Color(0xffF5F5F5),
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 9.0),
child: Row(children: [
Text(label,
style: TextStyle(
color: Color(0xff333333),
fontSize: 12,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
label != "東京五輪"
? Padding(
padding: EdgeInsets.only(left: 15.0),
child: Container(
height: 20.0, width: 0.4, color: Colors.grey))
: Container()
])));
}
}
Next, let’s look at SwiftUi.
I have used the HStack struct to represent the menu. To make it scroll horizontally, I used the ScrollView struct, which by default results in a vertical scroll, so I give it .horizontal value to make it scroll horizontally.
struct Menu2: View {
@Binding var selectedPage: Int
let menu2s = ["すべて","StayHome","ニュース","クーポン","芸能","スポーツ","話題","フォロー","東京五輪"]
var body: some View {
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(0..<9) { index in
Button(
action: {
self.selectedPage = index
})
{
Text(self.menu2s[index])
.foregroundColor(self.selectedPage == index ? .white : .gray)
.font(.system(size: 13))
.fontWeight(.bold)
}
.padding(EdgeInsets(top: 12, leading: 10, bottom: 12, trailing: 10))
.background(self.selectedPage == index ? Color.init(hex: "ed615b") : Color.init(hex: "eee"))
Divider().padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
}
}
}
Divider()
}
}
}
News List 1: Latest News
There are four UI patterns for news content. The first one.
This is the smallest of the news content, with six of these arranged as a single news item, with a photo on the left, title and update date on the right.
First, let’s look at the Flutter. I implemented the NewsList class with a photo on the left and a title, update date, etc. on the right, which is one news content. I used an expanded widget to always display the photo on the left and the title, update date, etc. on the right in the same ratio.
By placing six NewsList classes inside the Column widget, I have completed News List 1.
class NewsList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
child: Row(children: [
Expanded(
flex: 1,
child: SizedBox(
width: 50.0,
height: 50.0,
child: Image.asset("assets/images/150x150.png"),
)),
SizedBox(width: 10),
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("東京の感染者数 経路不明5割弱",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Row(children: [
Icon(Icons.chat_bubble_outline,
size: 12, color: Colors.pinkAccent),
SizedBox(width: 3),
Text("47",
style: TextStyle(
fontSize: 12.0, color: Colors.pinkAccent)),
SizedBox(width: 10),
Text("7/12(日) 0:24",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
SizedBox(width: 10),
Container(
padding: EdgeInsets.symmetric(horizontal: 4.0),
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
color: Colors.orangeAccent),
child: Text("NEW",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 10.0,
color: Colors.white,
letterSpacing: -0.5)),
)
])
]))
]));
}
}
Next, let’s look at SwiftUi.
I have implemented a single news content, the NewsList struct, with a photo on the left, a title and update date on the right, etc. I use the VStack struct and the HStack struct to represent the UI.
the NewsList struct is placed in the VStack struct, and NewsList 1 is completed.
struct NewsList: View {
fileprivate func newsDetail() -> some View {
return HStack(spacing: 0.0){
Image("150x150").resizable().frame(width: 50.0, height: 50.0)
Spacer().frame(width:10)
VStack(alignment: .leading){
Text("東京の感染者数 経路不明5割弱")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
HStack{
Image(systemName: "bubble.right")
.resizable()
.frame(width: 12.0, height: 12.0, alignment: .center)
.foregroundColor(Color.init(hex: "FF4081"))
Spacer().frame(width:1)
Text("47")
.font(.system(size: 12))
.foregroundColor(Color.init(hex: "FF4081"))
Text("7/12(日) 0:24")
.font(.system(size: 12))
.foregroundColor(Color.init(hex: "9E9E9E"))
Text("NEW")
.padding(EdgeInsets.init(top: 2.0, leading: 4.0, bottom: 2.0, trailing: 4.0))
.font(.system(size: 10))
.foregroundColor(.white)
.background(Color.init(hex: "FFAB40"))
.cornerRadius(30)
}
Divider()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0){
newsDetail()
}
}
}
News list 2: The biggest news of all
Place the photo across the width of the screen, and underneath the photo I can see the title of the news and the source of this news.
First, let’s look at the Flutter.
Since I can place the photo, the news title, and the source of the news in order from the top, I use the Column widget. I want all the widgets in the Column widget to be aligned to the left, so I apply the CrossAxisAlignment.start.
class BigNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Image.asset("assets/images/collecting.jpeg"),
Text("女性から寄生虫、刺身食べて侵入か? 日本",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("NN.co.jp",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]),
);
}
}
Next, let’s look at SwiftUi.
Similar to Flutter, I use the VStack struct, as I can place the photo, news title, and source in order from the top. I want all the UIs in the VStack struct to be left-aligned, so I apply .leading to the alignment property of the VStack struct.
struct BigNews: View {
fileprivate func newsDetail() -> some View {
return VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 200)
Text("女性から寄生虫、刺身食べて侵入か? 日本")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("News.co.jp")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0) {
newsDetail()
}
}
}
News list 3: The Second Biggest News
First, let’s look at the Flutter.
The content of each news item is the same as News list 2. But the width is different.
Use the Row widget to place the two news items using half the width of the screen.
class MiddleNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Row(children: [
Container(
decoration: BoxDecoration(
border: Border(
right: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.only(right: 5.0),
width: (MediaQuery.of(context).size.width / 2) - 10.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset("assets/images/collecting.jpeg"),
Text("「100万円たまる貯金箱」を6年かけて満杯に 2000日の苦労を本人い聞いた",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("タウンネット",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
])),
Container(
padding: EdgeInsets.only(left: 5.0),
width: (MediaQuery.of(context).size.width / 2) - 10.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset("assets/images/collecting.jpeg"),
Text("「当時は睡眠2時間」、再結成、引退。。。紆余曲折から。。。",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("スポーツニュース",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
])),
]));
}
}
Next, let’s look at SwiftUi.
Similar to Flutter, the content of each news item is the same as News list 2. But with a different width.
struct MiddleNews: View {
fileprivate func newsDetail() -> some View {
return HStack {
VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 150)
Text("「100万円たまる貯金箱」を6年かけて満杯に 2000日の苦労を本人い聞いた")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("タウンネット")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
VStack(alignment: .leading, spacing: 0.0) {
Image("collecting").resizable()
.frame(height: 150)
Text("「当時は睡眠2時間」解散、再結成、引退。。。紆余曲折から。。。")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("スポーツニュース")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
}
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0) {
newsDetail()
}
}
}
News list 4: News with the simplest UI
First, let’s look at the Flutter.
The content of each news item is the same as News list 2. But the placement is slightly different. The photo is on the left, and the news title and source are on the right.
The division of the left and right sides is realized with the Row widget, and the vertical arrangement of the news title and source is realized with the Column widget.
class SmallNews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
child: Row(children: [
Expanded(
flex: 2,
child: Image.asset("assets/images/150x150.png"),
),
SizedBox(width: 10),
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("映画4作品の再上映。異例のヒット",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("シネマシネマ",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]))
]));
}
}
Next, let’s look at SwiftUi.
As with Flutter, the content of each news item is the same as News List 2. But the placement is slightly different. The photo is on the left and the news title and source are on the right.
The division of the left and right sides is achieved using the HStack struct, while the vertical alignment of the news title and source is achieved using the VStack struct.
struct SmallNews: View {
fileprivate func newsDetail() -> some View {
return HStack(spacing: 0.0){
Image("150x150").resizable().frame(width: 120.0, height: 120.0)
Spacer().frame(width:10)
VStack(alignment: .leading){
Text("映画4作品の再上映。異例のヒット")
.font(.system(size: 15))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("タウンネット")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0){
newsDetail()
}
}
}
Video News
First, let’s look at the Flutter.
I used the video_player package for video playback.
In the Column widget, I place video_player to place the video widget, the news title and the source of the video.
class BigMovie extends StatefulWidget {
@override
_BigMovie createState() => _BigMovie();
}
class _BigMovie extends State<BigMovie> {
VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4')
..initialize().then((_) {
_controller.setLooping(true);
_controller.play();
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
padding: EdgeInsets.all(5.0),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller))
: Container(),
Text("女性から寄生虫、刺身食べて侵入か? 日本",
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xff333333))),
SizedBox(width: 10),
Text("NN.co.jp",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey,
textBaseline: TextBaseline.ideographic)),
]),
);
}
}
Next, let’s look at SwiftUi.
I created the PlayerView struct for video playback, and performed from DL to playback.
I used the code introduced in this site.
Place the PlayerView struct, the news title, and the source in the VStack struct.
struct BigMovie: View {
fileprivate func movieDetail() -> some View {
return VStack(alignment: .leading, spacing: 0.0) {
PlayerView().frame(height: 200, alignment: .center)
Text("女性から寄生虫、刺身食べて侵入か? 日本")
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.init(hex: "333333"))
Spacer().frame(height:10)
Text("NN.co.jp")
.font(.system(size: 12))
.foregroundColor(Color.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
}
var body: some View {
VStack(spacing: 0.0) {
movieDetail()
}
}
}
scroll
Scrolling up locks the search bar, the scrolling menu to the top, and the rest of the content disappears to the top of the screen.
Scrolling down brings up the content that has disappeared to the top.
First, let’s look at the Flutter.
I used the CustomScrollView widget, SliverAppBar widget, SliverToBoxAdapter widget. I apply the Login widget and the Menu1 widget to the flexibleSpace property of the SliverAppBar widget. For the bottom property, I apply content that doesn’t disappear when scrolling to the top. In this case, I apply the menu2 widget, which is a scrolling menu.
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
backgroundColor: Color(0xffFFFAFA),
expandedHeight: 242,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Column(children: [Login(), PickUpNews(), Menu1()]))),
bottom: PreferredSize(
preferredSize: Size.fromHeight(-6), child: Menu2())),
SliverToBoxAdapter(
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_newsList(),
_bigNewsList(),
_middleNewsList(),
_smallNewsList(),
_bigMovieList(),
]))
],
);
}
}
Next, let’s look at SwiftUi. I use the List struct to achieve this. First, I apply the content that is displayed when not scrolling, but disappears when scrolled, to the VStack struct, which can be the Login class and the Menu1 class. The part that is not deleted when the content is scrolled is applied to the header property of the section struct. The Menu2 class is applied here, and the class of each menu list displayed at the bottom of the Menu2 class is applied to the content property.
var body: some View {
VStack(spacing: 0) {
SearchBar()
List {
VStack(spacing: 0) {
Login()
PickupNews()
Menu1()
}
.background(Color.white)
.listRowInsets(EdgeInsets())
Section(header: Menu2(selectedPage: $selectedPage)) {
self.minNewsList()
self.minNewsList()
self.bigNewsList()
self.middleNewsList()
self.smallNewsList()
self.bigMovieList()
}
.listRowInsets(EdgeInsets())
}
}
}
summary
This time, I built a UI for a famous Japanese news app in Flutter and SwiftUi, but I found that it took time and effort to make the font family, font size, padding, margin, etc. exactly the same for both Flutter and SwiftUi. For the implementation of declarative UI, I didn’t notice any significant differences in terms of ease of writing or source visibility. (Although Flutter was a little easier to write due to familiarity)
For those of us who have been using storyboard in iOS development, SwiftUi may have some resistance, but once you get used to it, it’s faster, lighter and more like programming.
I’d like to implement more screen transitions and behaviors of the widgets in the screen, but I’ve decided to implement the static part. I’d also like to compare the ui rebuild. I am curious about it.