19、仿 Instagram 应用开发:功能完善与界面优化

仿 Instagram 应用开发:功能完善与界面优化

在开发仿 Instagram 应用的过程中,我们需要对多个屏幕进行优化和功能添加,包括个人资料屏幕、搜索屏幕、收藏屏幕以及对主页屏幕的优化。以下将详细介绍这些屏幕的开发过程和相关代码实现。

1. 个人资料屏幕

个人资料屏幕是展示用户信息和帖子的重要界面。我们需要对其进行更新,确保各个视觉元素与 ProfileViewController 类中的对应出口连接。

  • 界面布局与连接
    • 打开故事板,更新个人资料屏幕。使用约束布局,将用户头像( UIImageView )、用户名( UILabel )和展示用户帖子的 UICollectionView 等元素与 ProfileViewController 类中的出口连接。
    • 其余 UI 元素存储一些模拟数据,如帖子数量、关注者数量和关注的人数。
    • 放置一个“退出登录”按钮,但不要将其放在前排,以保持用户在应用内的停留。
class ProfileViewController: UIViewController {
    var userUDID: String? = nil
    var listOfPosts: [PostModel]?
    @IBOutlet weak var avatarImageView: UIImageView!
    @IBOutlet weak var username: UILabel!
    @IBOutlet weak var posts: UICollectionView!
    @IBOutlet weak var followButton: UIButton!
    @IBOutlet weak var logoutButton: UIButton!
    @IBOutlet var avatarGestureRecogniser: UITapGestureRecognizer!
    @IBOutlet var usernameTapGestureRecogniser: UITapGestureRecognizer!
    private let photoCellReuseIdentifier = "PhotoCell"
    private var pickedImage: UIImage?
    ...
}
  • 退出登录功能
    当点击“退出登录”按钮时,触发 logoutHandler 函数,该函数会将当前登录用户退出,并移除共享 DataManager 实例中存储的用户引用。
@IBAction func logoutHandler(_ sender: Any) {
    let authUI = FUIAuth.defaultAuthUI()
    do {
        try authUI?.signOut()
        let nc = NotificationCenter.default
        nc.post(name: Notification.Name(rawValue: "userSignedOut"),
                object: nil,
                userInfo: nil)
        // remove the active user
        DataManager.shared.user = nil
        DataManager.shared.userUID = nil
    } catch let error {
        print("Error: \(error)")
    }
}
  • 视图加载时的 UI 调整
    在视图控制器加载时,需要调整 UI,以适应不同的用户资料。
override func viewDidLoad() {
    super.viewDidLoad()
    let cellNib = UINib(nibName: "PhotoViewCell", bundle: nil)
    posts.register(cellNib, forCellWithReuseIdentifier: photoCellReuseIdentifier)
    posts.dataSource = self
    // default avatar icon
    avatarImageView.image = #imageLiteral(resourceName: "user")
    username.text = userUDID ?? DataManager.shared.userUID
    if let layout = posts.collectionViewLayout as? UICollectionViewFlowLayout {
        let imageWidth = (UIScreen.main.bounds.width - 10) / 3
        layout.itemSize = CGSize(width: imageWidth, height: imageWidth)
    }
    // you can't follow yourself
    if userUDID == nil {
        followButton.isHidden = true
    } else {
        // disable change of avatar photo
        avatarGestureRecogniser.isEnabled = false
        // disable change of the username
        usernameTapGestureRecogniser.isEnabled = false
        logoutButton.isHidden = true
        // hide follow button
        if userUDID == DataManager.shared.userUID {
            followButton.isHidden = true
        }
    }
    loadData()
}
  • 创建 PhotoViewCell 组件
    创建一个新的 Cocoa 组件 PhotoViewCell ,用于展示用户的帖子图片。
class PhotoViewCell: UICollectionViewCell {
    @IBOutlet weak var image: UIImageView!
}
  • 添加手势和更新 Firebase 规则
    为头像和用户名添加 UITapGestures ,并确保 UI 组件启用用户交互。同时,需要更新 Firebase 数据库规则,允许登录用户读取所有资料,但只有资料所有者可以更新自己的资料。
avatarImageView.isUserInteractionEnabled = true
username.isUserInteractionEnabled = true

// Firebase 规则
"profile": {
    ".read": "auth != null",
    ".write": "false",
    "$uid": {
        ".read": "auth != null",
        ".write": "$uid === auth.uid"
    }
}
  • 头像上传功能
    当用户点击头像时,触发 pickAvatarImage 函数,使用 UIImagePickerController 让用户选择头像,并进行裁剪和缩放,最后上传到 Firebase 存储。
extension ProfileViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    @IBAction func pickAvatarImage(_ sender: Any) {
        let pickerController = UIImagePickerController()
        pickerController.delegate = self
        pickerController.allowsEditing = true
        present(pickerController, animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        if let editedImage = info[UIImagePickerControllerEditedImage] as? UIImage {
            pickedImage = self.scale(image: editedImage, toSize: CGSize(width: 100, height: 100))
        } else if let chosenImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
            pickedImage = self.scale(image: chosenImage, toSize: CGSize(width: 100, height: 100))
        }
        picker.dismiss(animated: true, completion: nil)
        // does the heavy lifting
        updateAvatar()
    }

    func updateAvatar() {
        if pickedImage != nil {
            self.avatarImageView.image = pickedImage
        }
        DataManager.shared.updateProfile(avatar: pickedImage, progress: { progress in
            print("Upload avatar progress: \(progress)")
        }) { result in
            if !result {
                print("something went wrong")
            }
        }
    }

    func scale(image: UIImage, toSize size: CGSize) -> UIImage? {
        let imageSize = image.size
        let widthRatio = size.width / image.size.width
        let heightRatio = size.height / image.size.height
        var newSize: CGSize
        if (widthRatio > heightRatio) {
            newSize = CGSize(width: imageSize.width * heightRatio, height: imageSize.height * heightRatio)
        } else {
            newSize = CGSize(width: imageSize.width * widthRatio, height: imageSize.height * widthRatio)
        }
        UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
        image.draw(in: CGRect(origin: CGPoint.zero, size: newSize))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage
    }
}
  • 用户名更改功能
    当用户点击用户名时,弹出一个带有文本输入框的警告框,用户可以输入新的用户名。点击“更新”按钮后,将新用户名保存到数据库。
@IBAction func changeUsername(_ sender: Any) {
    let alertController = UIAlertController(title: "Change your username",
                                            message: "Please, enter a new username.",
                                            preferredStyle: .alert)
    alertController.addTextField { (textField) in
        // do some textFiled customization
    }
    alertController.addAction(UIAlertAction(title: "Update",
                                            style: .default,
                                            handler: { [weak alertController, weak self] (action) in
                                                if let textFields = alertController?.textFields! {
                                                    if textFields.count > 0 {
                                                        let textFiled = textFields[0]
                                                        // update the ui
                                                        self?.username.text = textFiled.text
                                                        // update the server data
                                                        self?.updateUsername(username: textFiled.text)
                                                    }
                                                }
                                            }))
    alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
    self.present(alertController, animated: true, completion: nil)
}

func updateUsername(username: String?) {
    DataManager.shared.updateProfileUsername(username: username) { result in
        if !result {
            print("something went wrong")
        }
    }
}

// DataManager 中的更新用户名方法
func updateProfileUsername(username newUsername: String?, callback: @escaping (Bool) -> ()) {
    guard let userID = userUID else {
        callback(false)
        return
    }
    guard let username = newUsername else {
        callback(false)
        return
    }
    let dbKey = "profile/\(userID)/username"
    let childUpdates = [dbKey: username]
    databaseRef.updateChildValues(childUpdates)
    callback(true)
}
2. 搜索屏幕

搜索屏幕顶部有一个搜索栏,允许用户搜索符合条件的照片。

  • 创建 SearchViewController
    创建一个新的 SearchViewController.swift 文件,用于处理搜索屏幕的逻辑。
class SearchViewController: UIViewController {
    private let photoCellReuseIdentifier = "PhotoCell"
    var model: [PostModel]?
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        let cellNib = UINib(nibName: "PhotoViewCell", bundle: nil)
        collectionView.register(cellNib, forCellWithReuseIdentifier: photoCellReuseIdentifier)
        let gridLayout = GridLayout()
        gridLayout.fixedDivisionCount = 3
        gridLayout.scrollDirection = .vertical
        gridLayout.delegate = self
        collectionView.collectionViewLayout = gridLayout
        collectionView.dataSource = self
        searchBar.delegate = self
        loadData()
    }

    func loadData() {
        model = []
        DataManager.shared.fetchHomeFeed { [weak self] items in
            if items.count > 0 {
                self?.model? += items
                self?.collectionView.reloadData()
            }
        }
    }
}
  • 实现 GridLayoutDelegate UICollectionViewDataSource
    SearchViewController 扩展 GridLayoutDelegate UICollectionViewDataSource 协议,实现单元格的缩放和数据展示。
extension SearchViewController: GridLayoutDelegate {
    func scaleForItem(inCollectionView collectionView: UICollectionView,
                      withLayout layout: UICollectionViewLayout,
                      atIndexPath indexPath: IndexPath) -> UInt {
        if indexPath.row % 9 == 0 {
            return 2
        }
        return 1
    }
}

extension SearchViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView,
                        numberOfItemsInSection section: Int) -> Int {
        return model?.count ?? 0
    }

    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoCellReuseIdentifier,
                                                            for: indexPath) as? PhotoViewCell else {
            return UICollectionViewCell()
        }
        guard let post = model?[indexPath.row] else {
            return cell
        }
        if let image = post.photoURL {
            let imgRef = Storage.storage().reference().child(image)
            cell.image.sd_setImage(with: imgRef)
        }
        return cell
    }
}
  • 实现 UISearchBarDelegate
    SearchViewController 扩展 UISearchBarDelegate 协议,处理搜索功能。
extension SearchViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        if let searchText = searchBar.text {
            if !searchText.isEmpty {
                DataManager.shared.search(for: searchText) { [weak self] items in
                    self?.model? = items
                    self?.collectionView.reloadData()
                }
                searchBar.text = ""
                // hide the keyboard
                searchBar.resignFirstResponder()
            }
        }
    }
}

// DataManager 中的搜索方法
func search(for searchText: String, callback: @escaping ([PostModel]) -> ()) {
    let key = "description"
    databaseRef
      .child("posts")
      .queryOrdered(byChild: key)
      .queryStarting(atValue: searchText, childKey: key)
      .queryEnding(atValue: searchText + "\u{f8ff}", childKey: key)
      .observeSingleEvent(of: .value, with: { snapshot in
            let items: [PostModel] = snapshot.children.compactMap { child in
                guard let child = child as? DataSnapshot else {
                    return nil
                }
                return PostModel.init(snapshot: child)
            }
            DispatchQueue.main.async {
                callback(items)
            }
        })
}
3. 收藏屏幕

收藏屏幕用于展示用户的收藏帖子。如果收藏列表为空,将显示一个提示信息。

  • 创建 FavoritesViewController
    创建一个新的 FavoritesViewController 类,处理收藏屏幕的逻辑。
class FavoritesViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var noItems: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        showEmptyView()
        loadData()
    }

    func loadData() {
        // TODO: load all favorite posts
    }
}
  • 实现 EmptyCollectionView 协议
    定义一个 EmptyCollectionView 协议,用于切换收藏列表和空视图的显示。
protocol EmptyCollectionView {
    func showCollectionView()
    func showEmptyView()
    var collectionView: UICollectionView! { get }
    var emptyView: UIView? { get }
}

extension EmptyCollectionView {
    func showCollectionView() {
        self.emptyView?.isHidden = true
        self.collectionView.isHidden = false
    }

    func showEmptyView() {
        if self.emptyView != nil {
            self.emptyView?.isHidden = false
            self.collectionView.isHidden = true
        }
    }
}

extension FavoritesViewController: EmptyCollectionView {
    var emptyView: UIView? {
        return noItems
    }
}
4. 主页屏幕优化

对主页屏幕进行优化,包括加载用户资料、显示用户照片和点击头像或用户名打开个人资料屏幕。

  • 添加新属性和模型
    HomeFeedViewController 中添加一个字典 users 用于存储用户资料,并在 DataManager 中添加 UserModel 类。
// HomeFeedViewController 中添加
var users = [String: UserModel?]()

// DataManager 中添加
class UserModel {
    var avatarPhoto: String?
    var username: String?
    init() {
        // nothing
    }
    init?(snapshot: DataSnapshot) {
        if let dict = snapshot.value as? [String: Any] {
            if dict["avatar"] != nil {
                self.avatarPhoto = dict["avatar"] as? String
            }
            if dict["username"] != nil {
                self.username = dict["username"] as? String
            }
        } else {
            return nil
        }
    }
}
  • 加载用户资料
    HomeFeedViewController 中添加 loadAllUsers 函数,用于加载所有用户的资料。
func loadAllUsers() {
    var usersInfoToLoad = 0
    var usersInfoLoaded = 0
    if let model = self.model {
        for item in model {
            let userId = item.author
            if users[userId] == nil {
                usersInfoToLoad += 1
                users[userId] = UserModel()
            }
        }
        let reloadView = { [weak self] in
            if usersInfoLoaded == usersInfoToLoad {
                self?.collectionView.reloadData()
            }
        }
        for author in users.keys {
            let userId = author
            DataManager.shared.loadUserInfo(userId: userId) { [weak self] userModel in
                if let userModel = userModel {
                    self?.users[userId] = userModel
                    usersInfoLoaded += 1
                    // update the UI if we loaded everything
                    reloadView()
                }
            }
        }
    }
}

// 更新 loadData 函数
func loadData() {
    model = []
    DataManager.shared.fetchHomeFeed { [weak self] items in
        if items.count > 0 {
            self?.model? += items
            self?.loadAllUsers()
            self?.collectionView.reloadData()
        }
    }
}
  • 更新单元格数据
    在填充单元格数据的方法中,添加用户资料的显示。
cell.avatarImage.image = #imageLiteral(resourceName: "user")
// update the user info
if let user = self.users[post.author] {
    cell.avatarName.text = user?.username ?? post.author
    if let avatarPath = user?.avatarPhoto {
        let imgRef = Storage.storage().reference().child(avatarPath)
        cell.avatarImage.sd_setImage(with: imgRef, placeholderImage: #imageLiteral(resourceName: "user"), completion: nil)
    }
}
  • 添加手势和协议
    FeedViewCell 类添加手势识别器和 ProfileHandler 协议,处理点击头像或用户名打开个人资料屏幕的功能。
protocol ProfileHandler {
    func openProfile(cell: UICollectionViewCell)
}

class FeedViewCell: UICollectionViewCell {
    // old code is here ... except awakeFromNib
    var tapGestureRecogniser: UITapGestureRecognizer!
    var delegate: ProfileHandler?

    override func awakeFromNib() {
        super.awakeFromNib()
        translatesAutoresizingMaskIntoConstraints = false
        self.contentView.translatesAutoresizingMaskIntoConstraints = false
        avatarImage.layer.cornerRadius = avatarImage.frame.height / 2
        avatarImage.clipsToBounds = true
        // new lines
        tapGestureRecogniser = UITapGestureRecognizer(target: self,
                                                      action: #selector(onProfileTap))
        avatarName.superview?.addGestureRecognizer(tapGestureRecogniser)
    }

    @objc func onProfileTap(sender: Any) {
        delegate?.openProfile(cell: self)
    }
}

extension HomeFeedViewController: ProfileHandler {
    func openProfile(cell: UICollectionViewCell) {
        guard let indexPath = self.collectionView.indexPath(for: cell),
              let post = model?[indexPath.row] else {
            return
        }
        performSegue(withIdentifier: "openProfile", sender: post.author)
    }
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "openProfile" {
        if let navController = segue.destination as? UINavigationController {
            if let profileVC = navController.topViewController as? ProfileViewController {
                profileVC.userUDID = sender as? String
            }
        }
    }
}

// 设置单元格的代理
cell.delegate = self

通过以上步骤,我们完成了仿 Instagram 应用的多个屏幕的开发和优化,包括个人资料屏幕、搜索屏幕、收藏屏幕和主页屏幕。每个屏幕都能从 Firebase 加载真实数据,使应用更加完善和实用。

以下是各个屏幕的开发流程总结:
| 屏幕名称 | 开发步骤 |
| ---- | ---- |
| 个人资料屏幕 | 1. 更新界面布局并连接元素
2. 实现退出登录功能
3. 调整视图加载时的 UI
4. 创建 PhotoViewCell 组件
5. 添加手势和更新 Firebase 规则
6. 实现头像上传和用户名更改功能 |
| 搜索屏幕 | 1. 创建 SearchViewController
2. 实现 GridLayoutDelegate UICollectionViewDataSource
3. 实现 UISearchBarDelegate 处理搜索功能 |
| 收藏屏幕 | 1. 创建 FavoritesViewController
2. 实现 EmptyCollectionView 协议切换视图显示 |
| 主页屏幕 | 1. 添加新属性和模型
2. 加载用户资料
3. 更新单元格数据显示用户信息
4. 添加手势和协议处理打开个人资料屏幕功能 |

graph LR
    A[个人资料屏幕] --> B[更新界面布局]
    A --> C[实现退出登录]
    A --> D[调整 UI]
    A --> E[创建组件]
    A --> F[添加手势和规则]
    A --> G[实现上传和更改功能]
    H[搜索屏幕] --> I[创建控制器]
    H --> J[实现布局和数据源协议] 
    H --> K[实现搜索代理协议]
    L[收藏屏幕] --> M[创建控制器]
    L --> N[实现视图切换协议]
    O[主页屏幕] --> P[添加属性和模型]
    O --> Q[加载用户资料]
    O --> R[更新单元格数据]
    O --> S[添加手势和协议]

通过以上的开发和优化,我们的仿 Instagram 应用在功能和用户体验上都有了显著的提升。各个屏幕之间的交互更加流畅,用户可以方便地查看和管理自己的资料、搜索照片以及收藏喜欢的帖子。同时,我们还通过 Firebase 实现了数据的存储和读取,确保了应用的稳定性和可靠性。在后续的开发中,我们可以进一步完善应用的功能,如添加更多的社交互动功能、优化搜索算法等,以满足用户的更多需求。

仿 Instagram 应用开发:功能完善与界面优化

5. 导航与界面细节处理

在开发过程中,导航和界面的细节处理对于提升用户体验至关重要。我们需要确保各个屏幕之间的导航流畅,并且处理好一些特殊情况。

  • 创建导航控制器和 segue
    为了实现从主页屏幕点击头像或用户名打开个人资料屏幕的功能,我们需要创建一个新的 UINavigationController ,并将 ProfileViewController 作为其根视图控制器。同时,添加一个名为 openProfile 的 segue。

操作步骤如下:
1. 在故事板中,添加一个新的 UINavigationController
2. 将 ProfileViewController 设置为该导航控制器的根视图控制器。
3. 在 HomeFeedViewController 中,为点击头像或用户名的操作添加 openProfile segue。

// 在 HomeFeedViewController 中触发 segue
extension HomeFeedViewController: ProfileHandler {
    func openProfile(cell: UICollectionViewCell) {
        guard let indexPath = self.collectionView.indexPath(for: cell),
              let post = model?[indexPath.row] else {
            return
        }
        performSegue(withIdentifier: "openProfile", sender: post.author)
    }
}

// 处理 segue 传递数据
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "openProfile" {
        if let navController = segue.destination as? UINavigationController {
            if let profileVC = navController.topViewController as? ProfileViewController {
                profileVC.userUDID = sender as? String
            }
        }
    }
}
  • 处理特殊情况
    当从主页屏幕点击头像进入个人资料屏幕时, UINavigationBar 会显示。同时,需要根据不同情况处理“退出登录”按钮和“关注”按钮的显示。
// 在 ProfileViewController 中根据情况处理按钮显示
override func viewDidLoad() {
    super.viewDidLoad()
    // ... 其他代码 ...
    if userUDID == nil {
        followButton.isHidden = true
    } else {
        if userUDID == DataManager.shared.userUID {
            followButton.isHidden = true
            logoutButton.isHidden = false
        } else {
            followButton.isHidden = false
            logoutButton.isHidden = true
        }
    }
}
6. 应用整体效果与展望

经过一系列的开发和优化,我们的仿 Instagram 应用已经具备了多个核心功能,并且各个屏幕都能从 Firebase 加载真实数据,整体效果接近我们的初始设想。

  • 应用效果展示
    以下是应用在模拟器上的一些截图:

    • 主页屏幕:展示了用户的帖子列表,每个帖子包含头像、用户名和照片。
    • 搜索屏幕:用户可以输入关键词搜索符合条件的照片。
    • 收藏屏幕:如果没有收藏帖子,会显示提示信息;有收藏帖子时,展示收藏列表。
    • 个人资料屏幕:展示用户的信息和帖子,用户还可以更改头像和用户名。
  • 未来优化方向
    虽然应用已经具备了基本功能,但仍有一些可以优化和扩展的方向:

    1. 社交互动功能 :添加点赞、评论、分享等社交互动功能,增强用户之间的交流。
    2. 搜索算法优化 :目前的搜索功能只能根据描述的开头进行匹配,未来可以使用第三方 API 实现更强大的全文搜索。
    3. 性能优化 :优化数据加载和 UI 渲染,提高应用的响应速度和流畅度。
    4. 界面设计优化 :进一步美化界面,提升用户体验。

以下是未来优化的优先级和简要说明:
| 优化方向 | 优先级 | 简要说明 |
| ---- | ---- | ---- |
| 社交互动功能 | 高 | 增强用户粘性和活跃度 |
| 搜索算法优化 | 中 | 提升搜索准确性和功能 |
| 性能优化 | 中 | 提高应用的响应速度和稳定性 |
| 界面设计优化 | 低 | 改善用户视觉体验 |

graph LR
    A[社交互动功能] --> B[点赞功能]
    A --> C[评论功能]
    A --> D[分享功能]
    E[搜索算法优化] --> F[使用第三方 API]
    E --> G[实现全文搜索]
    H[性能优化] --> I[优化数据加载]
    H --> J[优化 UI 渲染]
    K[界面设计优化] --> L[美化界面元素]
    K --> M[提升交互体验]

通过以上的开发和优化,我们的仿 Instagram 应用已经取得了显著的进展。未来,我们可以根据用户反馈和市场需求,持续对应用进行改进和扩展,使其更加完善和实用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值