仿 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 加载真实数据,整体效果接近我们的初始设想。
-
应用效果展示 :
以下是应用在模拟器上的一些截图:- 主页屏幕:展示了用户的帖子列表,每个帖子包含头像、用户名和照片。
- 搜索屏幕:用户可以输入关键词搜索符合条件的照片。
- 收藏屏幕:如果没有收藏帖子,会显示提示信息;有收藏帖子时,展示收藏列表。
- 个人资料屏幕:展示用户的信息和帖子,用户还可以更改头像和用户名。
-
未来优化方向 :
虽然应用已经具备了基本功能,但仍有一些可以优化和扩展的方向:- 社交互动功能 :添加点赞、评论、分享等社交互动功能,增强用户之间的交流。
- 搜索算法优化 :目前的搜索功能只能根据描述的开头进行匹配,未来可以使用第三方 API 实现更强大的全文搜索。
- 性能优化 :优化数据加载和 UI 渲染,提高应用的响应速度和流畅度。
- 界面设计优化 :进一步美化界面,提升用户体验。
以下是未来优化的优先级和简要说明:
| 优化方向 | 优先级 | 简要说明 |
| ---- | ---- | ---- |
| 社交互动功能 | 高 | 增强用户粘性和活跃度 |
| 搜索算法优化 | 中 | 提升搜索准确性和功能 |
| 性能优化 | 中 | 提高应用的响应速度和稳定性 |
| 界面设计优化 | 低 | 改善用户视觉体验 |
graph LR
A[社交互动功能] --> B[点赞功能]
A --> C[评论功能]
A --> D[分享功能]
E[搜索算法优化] --> F[使用第三方 API]
E --> G[实现全文搜索]
H[性能优化] --> I[优化数据加载]
H --> J[优化 UI 渲染]
K[界面设计优化] --> L[美化界面元素]
K --> M[提升交互体验]
通过以上的开发和优化,我们的仿 Instagram 应用已经取得了显著的进展。未来,我们可以根据用户反馈和市场需求,持续对应用进行改进和扩展,使其更加完善和实用。
超级会员免费看
1731

被折叠的 条评论
为什么被折叠?



