This time, I will implement an Instagram profile-like UI with UICollectionView with just code. Is the content
I hope this article helps someone
It's just that Swift beginners imitated the UI, so I think there are some points that can not be reached, If you notice any mistakes such as how to write the code, please give me some advice! !!
Three months before I started writing, Piempaon doesn't stop
[Instagrammer who really longs for entrepreneurs]
・ For beginners of iOS application development ・ Those who want to know how to use UICollectionView ・ Those who want to develop without using StoryBoard ・ Those who like Instagram UI
・ Version 11.3 (11C29) ・ Swift 5
I will put the source code below
https://github.com/Isseymiyamoto/FamousAppUI/tree/master/Instagram_profile/Instagram_profile
This time, we will add new files in the View
and Controller
folders because we will not perform communication such as data acquisition.
ʻUtils> ʻExtensions.swift
includes functions to simplify Layout-related processing,
I will not describe the details in this article, so it would be helpful if you copy and paste from Github.
Now let's move on to implementation
For 1 and 2, skip if you don't need TabBar
Here, let's create a Controller file to work with TabBar implemented in 2. Since there are 5 tab icons to display on Instagram, create 5 files directly under the Controller folder
ProfileController.swift
import UIKit
class ProfileController: UICollectionViewController{
override func viewDidLoad() {
super.viewDidLoad()
}
}
For the other 4 files, the following is fine
FeedController.swift
import UIKit
class FeedController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
//Enter the character string you want to display in the navigationBar
navigation.item = "Post"
}
}
In order to link the file created in 1 with TabBar, create MainTabController.swift
directly under the controller
folder.
MainTabController.swift
import UIKit
class MainTabController: UITabBarController{
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
configureViewControllers()
}
// MARK: - Helpers
func configureUI(){
view.backgroundColor = .white
tabBar.tintColor = .black
}
func configureViewControllers(){
let feed = FeedController()
let nav1 = templateNavigationController(image: UIImage(systemName: "house"), rootViewController: feed)
let search = SearchController()
let nav2 = templateNavigationController(image: UIImage(systemName: "magnifyingglass"), rootViewController: search)
let upload = UploadPostController()
let nav3 = templateNavigationController(image: UIImage(systemName: "plus.app"), rootViewController: upload)
let notification = NotificationController()
let nav4 = templateNavigationController(image: UIImage(systemName: "heart"), rootViewController: notification)
//Displayed as if 2 notifications were received
nav4.tabBarItem.selectedImage = UIImage(systemName: "heart.fill")
nav4.tabBarItem.badgeValue = "2"
let profile = ProfileController(collectionViewLayout: UICollectionViewFlowLayout())
let nav5 = templateNavigationController(image: UIImage(systemName: "person"), rootViewController: profile)
//Decide which controller to place in the tab bar
viewControllers = [nav1, nav2, nav3, nav4, nav5]
//Initial display of profileController
selectedIndex = 4
}
//Function to set any rootViewController, tabIcon image,Used within configureViewControllers
func templateNavigationController(image: UIImage?, rootViewController: UIViewController) -> UINavigationController{
let nav = UINavigationController(rootViewController: rootViewController)
nav.tabBarItem.image = image
nav.navigationBar.tintColor = .white
return nav
}
}
Now, let's edit SceneDelegate.swift
and setMainTabController
to be displayed at startup.
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
//abridgement
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: scene)
window?.rootViewController = MainTabController()
window?.makeKeyAndVisible()
}
//abridgement
}
It is perfect if the following is displayed when you start the Simulator after setting here
Next, let's create a View to apply to the Profile Controller. I will make it in the following format, but first let's make a ProfileHeaderCell of the profile outline part here
Create a file of ProfileHeader.swift
directly under View
ProfileHeader.swift
import UIKit
//It doesn't have to be because it's a haribote
protocol ProfileHeaderDelegate: class {
func handleEditProfile(_ header: ProfileHeader)
}
class ProfileHeader: UICollectionViewCell{
// MARK: - Properties
//It doesn't have to be because it's a haribote
weak var delegate: ProfileHeaderDelegate?
private let profileImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
iv.clipsToBounds = true
iv.image = UIImage(named: "Please put a photo appropriately")
iv.layer.borderColor = UIColor.black.cgColor
return iv
}()
private lazy var postCountButton = makeStatsButton(withNumber: "12")
private lazy var followingCountButton = makeStatsButton(withNumber: "320")
private lazy var followerCountButton = makeStatsButton(withNumber: "1000")
private lazy var postCountLabel = makeStatsTitle(withTitle: "Post")
private lazy var followingCountLabel = makeStatsTitle(withTitle: "following")
private lazy var followerCountLabel = makeStatsTitle(withTitle: "Follower")
private let fullnameLabel: UILabel = {
let label = UILabel()
label.text = "Onamae"
label.font = UIFont.boldSystemFont(ofSize: 14)
return label
}()
private let bioLabel: UILabel = {
let label = UILabel()
label.text = "This is an attempt to imitate the UI of Instagram profile. That's right. It just imitates."
label.font = UIFont.systemFont(ofSize: 14)
label.numberOfLines = 3
return label
}()
private let editProfileButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit profile", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(handleEditProfileButtonTapped), for: .touchUpInside)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
button.layer.borderColor = UIColor.lightGray.cgColor
button.layer.borderWidth = 1
button.layer.cornerRadius = 4
button.backgroundColor = .white
return button
}()
private let storiesPlusButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "plus"), for: .normal)
button.tintColor = .black
button.backgroundColor = .clear
button.layer.borderColor = UIColor.lightGray.cgColor
button.layer.borderWidth = 0.75
return button
}()
private let storiesPlusLabel: UILabel = {
let label = UILabel()
label.text = "New"
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 10)
return label
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemGroupedBackground
let postCountStack = makeStatsStackView(button: postCountButton, label: postCountLabel)
let followingCountStack = makeStatsStackView(button: followingCountButton, label: followingCountLabel)
let followerCountStack = makeStatsStackView(button: followerCountButton, label: followerCountLabel)
let infoStack = UIStackView(arrangedSubviews: [postCountStack, followingCountStack, followerCountStack])
infoStack.axis = .horizontal
infoStack.alignment = .center
infoStack.distribution = .fillEqually
addSubview(profileImageView)
profileImageView.anchor(top: safeAreaLayoutGuide.topAnchor, left: leftAnchor, paddingTop: 16, paddingLeft: 16)
profileImageView.setDimensions(width: 96, height: 96)
profileImageView.layer.cornerRadius = 96 / 2
addSubview(infoStack)
infoStack.centerY(inView: profileImageView)
infoStack.anchor(left: profileImageView.rightAnchor, right: rightAnchor, paddingLeft: 16, paddingRight: 32)
addSubview(fullnameLabel)
fullnameLabel.anchor(top: profileImageView.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 16, paddingLeft: 16, paddingRight: 16)
addSubview(bioLabel)
bioLabel.anchor(top: fullnameLabel.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 4, paddingLeft: 16, paddingRight: 16)
addSubview(editProfileButton)
editProfileButton.anchor(top: bioLabel.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 16, paddingLeft: 16, paddingRight: 16 )
addSubview(storiesPlusButton)
storiesPlusButton.anchor(top: editProfileButton.bottomAnchor, left: leftAnchor, paddingTop: 16, paddingLeft: 16)
storiesPlusButton.setDimensions(width: 64, height: 64)
storiesPlusButton.layer.cornerRadius = 64 / 2
addSubview(storiesPlusLabel)
storiesPlusLabel.centerX(inView: storiesPlusButton)
storiesPlusLabel.anchor(top: storiesPlusButton.bottomAnchor, paddingTop: 4)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Selectors
//It doesn't have to be because it's a haribote
@objc func handleEditProfileButtonTapped(){
delegate?.handleEditProfile(self)
}
// MARK: - Helpers
//For creating StackView that aligns the number of buttons and details vertically
fileprivate func makeStatsStackView(button: UIButton, label: UILabel) -> UIStackView{
let stack = UIStackView(arrangedSubviews: [button, label])
stack.axis = .vertical
stack.alignment = .center
stack.setDimensions(width: 160, height: 40)
return stack
}
//For creating display buttons such as the number of posts and followers
private func makeStatsButton(withNumber number: String) -> UIButton{
let button = UIButton(type: .system)
button.setTitle(number, for: .normal)
button.setTitleColor(.black, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
return button
}
//For creating labels to display details such as the number of posts and followers
private func makeStatsTitle(withTitle title: String) -> UILabel{
let label = UILabel()
label.text = title
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 14)
return label
}
}
Let's apply this to Profile Controller (skip it once in 5 and 6)
ProfileController.swift
import UIKit
private let profileHeaderCell = "ProfileHeaderCell"
class ProfileController: UICollectionViewController{
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Selectors
//Ornament
@objc func handleRightButtonTapped(){
print("DEBUG: you pressed the button..")
}
@objc func handleRefresh(){
//Do nothing because there is no data
collectionView.refreshControl?.beginRefreshing()
collectionView.refreshControl?.endRefreshing()
}
// MARK: - Helpers
//Overall UI settings
func configureUI(){
view.backgroundColor = .systemGroupedBackground
configureNavigationBar()
configureCollectionView()
//Swipe down to reload wind settings
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
collectionView.refreshControl = refreshControl
}
//Settings related to navigationBar
func configureNavigationBar(){
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .systemGroupedBackground
navigationController?.navigationBar.isTranslucent = false
navigationController?.navigationBar.shadowImage = UIImage()
navigationItem.title = "user_id"
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.horizontal.3"), style: .plain, target: self, action: #selector(handleRightButtonTapped))
}
func configureCollectionView(){
collectionView.backgroundColor = .systemGroupedBackground
//Profile Header registration
collectionView.register(ProfileHeader.self, forCellWithReuseIdentifier: profileHeaderCell)
//Place collectionView so that it does not cover tabBar
guard let tabHeight = tabBarController?.tabBar.frame.height else { return }
collectionView.contentInset.bottom = tabHeight
}
}
// MARK: - UICollectionViewDataSource / UICollectionViewDelegate
extension ProfileController{
//Set the number of sections to 1 for the time being → Change to 2 after setting PostCell
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
//Number of cells to be displayed in the section → Since only one Profile Header is required, 1
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
//Setting of cell to display
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileHeaderCell, for: indexPath) as! ProfileHeader
return cell
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ProfileController: UICollectionViewDelegateFlowLayout{
//Cell size setting → Please change the height appropriately
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 340)
}
}
If you start up the Simulator so far, the Profile Header part will be displayed! Maybe! Let's finish from here to the end quickly
FilterView is a filter part for displaying your own post list or a list tagged by your friends. Display as header with indexPath.section = 1 on ProfileController
Now, for FilterView, create it by installing UICollectionView inside UICollectionReusableView. Yes, that is, the FilterViewCell of UICollectionViewCell is also created in a separate file. let's do our best
ProfileFilterView.swift
import UIKit
private let profileHeaderCellIdentifier = "profileHeaderCell"
//Haribote
protocol ProfileFilterViewDelegate: class {
func filterView(_ view: ProfileFilterView, didSelect index: Int)
}
class ProfileFilterView: UICollectionReusableView {
// MARK: - Properties
//Haribote
weak var delegate: ProfileFilterViewDelegate?
//collectionView to be put on view
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .systemGroupedBackground
cv.delegate = self
cv.dataSource = self
return cv
}()
//Animate this guy to create a nicely selected feeling
private let underlineView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
//Borderline with profileHeaderCell
private let abovelineView: UIView = {
let view = UIView()
view.backgroundColor = .lightGray
return view
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.register(ProfileFilterCell.self, forCellWithReuseIdentifier: identifier)
//IsSelected at initialization=Determine which cell to be true
let selectedIndexPath = IndexPath(row: 0, section: 0)
collectionView.selectItem(at: selectedIndexPath, animated: true, scrollPosition: .left)
addSubview(collectionView)
//Expand the collection view to fill the parent view
collectionView.addConstraintsToFillView(self)
}
override func layoutSubviews() {
addSubview(abovelineView)
abovelineView.anchor(left: leftAnchor, bottom: topAnchor, width: frame.width, height: 0.5)
addSubview(underlineView)
underlineView.anchor(left: leftAnchor, bottom: bottomAnchor, width: frame.width / 2, height: 1)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - UICollectionViewDataSource
extension ProfileFilterView: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//Since there are two choices of tag or post, return 2 is ok
return ProfileFilterOptions.allCases.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! ProfileFilterCell
//Update option on cell side
let option = ProfileFilterOptions(rawValue: indexPath.row)
cell.option = option
return cell
}
}
// MARK: - UICollectionViewDelegate
extension ProfileFilterView: UICollectionViewDelegate{
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
//0 to the x coordinate of the cell touchUpInside the underlineView.Move in 3 seconds
let xPosition = cell?.frame.origin.x ?? 0
UIView.animate(withDuration: 0.3) {
self.underlineView.frame.origin.x = xPosition
}
//Haribote → Originally process and write so that the display image can be changed with ProfileController
delegate?.filterView(self, didSelect: indexPath.row)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ProfileFilterView: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let count = CGFloat(ProfileFilterOptions.allCases.count)
return CGSize(width: frame.width / count, height: frame.height)
}
//Installed so that there are no gaps between items
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
ProfileFilterViewCell.swift
import UIKit
//Posts or tagging Post list Determine which one
enum ProfileFilterOptions: Int, CaseIterable{
case post
case tag
var systemImage: UIImage? {
switch self {
case .post: return UIImage(systemName: "rectangle.split.3x3")
case .tag: return UIImage(systemName: "person.crop.rectangle")
}
}
}
class ProfileFilterViewCell: UICollectionViewCell{
// MARK: - Properties
//Post or tag post list Set to change the image of imageView when either or Nen is updated
var option: ProfileFilterOptions! {
didSet{ imageView.image = option.systemImage }
}
private var imageView: UIImageView = {
let iv = UIImageView()
return iv
}()
//Change tintColor depending on whether it is selected or not
override var isSelected: Bool {
didSet{
imageView.tintColor = isSelected ? .black : .lightGray
}
}
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(imageView)
imageView.tintColor = .lightGray
imageView.setDimensions(width: 24, height: 24)
imageView.center(inView: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Now, I'm out of breath to write an article, but let's move on to step 6.
Finally, let's quickly create a cell that just displays a photo! !!
PostCell.swift
import UIKit
class PostCell: UICollectionViewCell{
// MARK: - Properties
let postImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.borderColor = UIColor.white.cgColor
self.layer.borderWidth = 0.5
addSubview(postImageView)
postImageView.addConstraintsToFillView(self)
postImageView.center(inView: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I entered life easy mode at once, so let's match everything with ProfileController at the end! !!
ProfileController.swift
import UIKit
private let filterViewIdentifier = "filterView"
private let profileHeaderCellIdentifier = "profileHeaderCell"
private let postCellIdentifier = "postCell"
class ProfileController: UICollectionViewController{
// MARK: - Properties
//Create a Haribote UIIMage array that you want to adapt to the post cell location
private var imageArray: [UIImage?] =
[UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve")]
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Selectors
@objc func handleRightButtonTapped(){
print("DEBUG: you pressed the button..")
}
@objc func handleRefresh(){
//Do nothing because there is no data
collectionView.refreshControl?.beginRefreshing()
collectionView.refreshControl?.endRefreshing()
}
// MARK: - Helpers
func configureUI(){
view.backgroundColor = .systemGroupedBackground
configureNavigationBar()
configureCollectionView()
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
collectionView.refreshControl = refreshControl
}
func configureNavigationBar(){
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .systemGroupedBackground
navigationController?.navigationBar.isTranslucent = false
navigationController?.navigationBar.shadowImage = UIImage()
navigationItem.title = "user_id"
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.horizontal.3"), style: .plain, target: self, action: #selector(handleRightButtonTapped))
}
func configureCollectionView(){
collectionView.backgroundColor = .systemGroupedBackground
collectionView.register(ProfileHeader.self, forCellWithReuseIdentifier: profileHeaderCellIdentifier)
collectionView.register(PostCell.self, forCellWithReuseIdentifier: postCellIdentifier)
collectionView.register(ProfileFilterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: filterViewIdentifier)
guard let tabHeight = tabBarController?.tabBar.frame.height else { return }
collectionView.contentInset.bottom = tabHeight
//Assimilate FilterView with navigationBar when scrolling
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.sectionHeadersPinToVisibleBounds = true
}
}
// MARK: - UICollectionViewDataSource
extension ProfileController{
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch section {
case 0:
return 1
default:
//Place as many cells as you want to display
return imageArray.count
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch indexPath.section {
case 0:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileHeaderCellIdentifier, for: indexPath) as! ProfileHeader
return cell
default:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: postCellIdentifier, for: indexPath) as! PostCell
//Assign to image of cell
cell.postImageView.image = imageArray[indexPath.row]
return cell
}
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
//ProfileFilterView registration as header
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: filterViewIdentifier, for: indexPath) as! ProfileFilterView
return header
}
}
// MARK: - UICollectionViewDelegate
extension ProfileController{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if(indexPath.section == 1){
print("DEBUG: this item is \(indexPath.row)")
}
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ProfileController: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
switch section {
case 0:
return CGSize(width: 0, height: 0)
default:
let height: CGFloat = 50
return CGSize(width: view.frame.width, height: height)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch indexPath.section {
case 0:
let height: CGFloat = 340
return CGSize(width: view.frame.width, height: height)
default:
//3 columns display, square size
let size = view.frame.width / 3
return CGSize(width: size, height: size)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
How about that? Have you completed the Instagram profile-like Haribote UI?
Towards the end, I'm getting tired and less talkative. I would like to add that if you let me know in the comments, such as where the explanation is insufficient.
Or rather, I'm sorry if there is a complaint that it doesn't work even if I copy it. I'll fix it right away.
Well then!
Recommended Posts